package cn.iocoder.yudao.module.mes.service.qc.seedquality;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.json.JSONUtil;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
|
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRespVO;
|
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.controller.admin.qc.seedquality.vo.MesQcSeedReturnQualitySubmitReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
|
import cn.iocoder.yudao.module.mes.framework.qrcode.MesTraceCodeUtils;
|
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
|
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.pro.pallet.MesProPalletDO;
|
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.returnsales.MesWmReturnSalesDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum;
|
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.qc.MesQcApproveNodeEnum;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApprovalTypeEnum;
|
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.qc.approver.MesQcApproverService;
|
import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
|
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesLineService;
|
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
|
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.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
import org.springframework.validation.annotation.Validated;
|
import java.math.BigDecimal;
|
import java.sql.Timestamp;
|
import java.time.LocalDateTime;
|
import java.util.*;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
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 int MAX_BATCH_SIZE = 500, WRITE_BATCH_SIZE = 100;
|
private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_INITIAL_FAIL_REVIEW = 25, WAIT_COMPANY = 30,
|
RELEASED = 40, WAIT_INITIAL_RECHECK = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
|
/** 质检线:1 加工部;2 销售部 */
|
private static final int QC_LINE_PROCESS = 1, QC_LINE_SALES = 2;
|
/** 业务类型:1 生产入库质检;2 经销商退货质检 */
|
private static final int BIZ_TYPE_PRODUCTION = 1, BIZ_TYPE_RETURN = 2;
|
/** 加工部领导审批流程定义标识 */
|
private static final String LINE_PROCESS_KEY = "mes_seed_quality_line_approve";
|
/** 销售部领导审批流程定义标识 */
|
private static final String SALES_LINE_PROCESS_KEY = "mes_seed_quality_sales_line_approve";
|
/** 公司质检审批流程定义标识 */
|
private static final String COMPANY_PROCESS_KEY = "mes_seed_quality_company_approve";
|
/** 经销商退货质检审批流程定义标识 */
|
private static final String RETURN_PROCESS_KEY = "mes_seed_return_approve";
|
@Resource private MesQcSeedQualityMapper qualityMapper;
|
@Resource private BpmProcessInstanceApi processInstanceApi;
|
@Resource private MesWmReturnSalesService returnSalesService;
|
@Resource private MesWmReturnSalesLineService returnSalesLineService;
|
@Resource private MesQcApproverService approverService;
|
@Resource private MesQcSeedQualityActionMapper actionMapper;
|
@Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
|
@Resource private MesProBagCodeMapper bagCodeMapper;
|
@Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper;
|
@Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper batchMapper;
|
@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;
|
@Resource private MesMdmItemService mdmItemService;
|
@Resource private MesQcSeedIndicatorRangeService rangeService;
|
|
@Override
|
public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) {
|
resolveBagScanCode(reqVO);
|
return qualityMapper.selectPage(reqVO);
|
}
|
|
@Override
|
public PageResult<MesQcSeedQualityBatchRespVO> getQualityBatchPage(MesQcSeedQualityPageReqVO reqVO) {
|
resolveBagScanCode(reqVO);
|
int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
|
int pageSize = reqVO.getPageSize() == null || reqVO.getPageSize() < 1 ? 10 : Math.min(reqVO.getPageSize(), 100);
|
long total = qualityMapper.countBatchGroups(reqVO);
|
if (total == 0) {
|
return new PageResult<>(Collections.emptyList(), 0L);
|
}
|
List<MesQcSeedQualityBatchRespVO> list = qualityMapper.selectBatchGroups(reqVO, (long) (pageNo - 1) * pageSize, pageSize)
|
.stream().map(this::toBatchRespVO).collect(Collectors.toList());
|
// 回填批次与品种信息
|
Set<Long> batchIds = list.stream().map(MesQcSeedQualityBatchRespVO::getBatchId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Map<Long, MesProBatchDO> batchMap = batchIds.isEmpty() ? Collections.emptyMap()
|
: batchMapper.selectBatchIds(batchIds).stream()
|
.collect(Collectors.toMap(MesProBatchDO::getId, Function.identity(), (first, ignored) -> first));
|
Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(list.stream()
|
.map(MesQcSeedQualityBatchRespVO::getItemId).filter(Objects::nonNull).collect(Collectors.toSet()));
|
for (MesQcSeedQualityBatchRespVO vo : list) {
|
vo.setBatchLabel(StrUtil.blankToDefault(vo.getBatchCode(), "未绑定批次"));
|
MesProBatchDO batch = batchMap.get(vo.getBatchId());
|
if (batch != null) {
|
vo.setLineName(batch.getLineName());
|
vo.setProduceDate(batch.getProduceDate());
|
}
|
MesMdmItemDO item = itemMap.get(vo.getItemId());
|
if (item != null) {
|
vo.setItemCode(item.getCode());
|
vo.setItemName(item.getName());
|
}
|
}
|
return new PageResult<>(list, total);
|
}
|
|
private void resolveBagScanCode(MesQcSeedQualityPageReqVO reqVO) {
|
if (StrUtil.isNotBlank(reqVO.getCode())) {
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(reqVO.getCode());
|
if (bag != null) {
|
reqVO.setCode(bag.getCode());
|
}
|
}
|
}
|
|
private MesQcSeedQualityBatchRespVO toBatchRespVO(Map<String, Object> row) {
|
MesQcSeedQualityBatchRespVO vo = new MesQcSeedQualityBatchRespVO();
|
vo.setBatchCode((String) row.get("batchCode"));
|
vo.setBatchId(toLong(row.get("batchId")));
|
vo.setItemId(toLong(row.get("itemId")));
|
vo.setTotalCount(toInt(row.get("totalCount")));
|
vo.setPassCount(toInt(row.get("passCount")));
|
vo.setFailCount(toInt(row.get("failCount")));
|
vo.setProcessingCount(toInt(row.get("processingCount")));
|
vo.setPendingRetestCount(toInt(row.get("pendingRetestCount")));
|
vo.setPendingProcessReviewCount(toInt(row.get("pendingProcessReviewCount")));
|
vo.setPendingCompanyReviewCount(toInt(row.get("pendingCompanyReviewCount")));
|
vo.setReleasedCount(toInt(row.get("releasedCount")));
|
vo.setScrappedCount(toInt(row.get("scrappedCount")));
|
vo.setLastInspectedAt(toLocalDateTime(row.get("lastInspectedAt")));
|
return vo;
|
}
|
|
private static Long toLong(Object value) {
|
return value instanceof Number number ? number.longValue() : null;
|
}
|
|
private static Integer toInt(Object value) {
|
return value instanceof Number number ? number.intValue() : 0;
|
}
|
|
private static LocalDateTime toLocalDateTime(Object value) {
|
if (value instanceof LocalDateTime dateTime) {
|
return dateTime;
|
}
|
if (value instanceof Timestamp timestamp) {
|
return timestamp.toLocalDateTime();
|
}
|
return null;
|
}
|
|
@Override
|
public Map<String, Object> resolveScan(String code) {
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(MesTraceCodeUtils.extractCode(code));
|
if (bag == null) throw new IllegalArgumentException("未解析到有效袋码,请扫描袋码");
|
Map<String, Object> result = new HashMap<>();
|
result.put("code", bag.getCode());
|
result.put("businessCode", bag.getBusinessCode());
|
result.put("batchCode", bag.getBatchCode());
|
result.put("inbound", bag.getInboundEventId() != null);
|
result.put("canceled", Integer.valueOf(30).equals(bag.getStatus()));
|
Long itemId = bag.getItemId();
|
MesMdmItemDO item = itemId == null ? null : mdmItemService.getItem(itemId);
|
result.put("itemId", itemId);
|
result.put("itemCode", item == null ? null : item.getCode());
|
result.put("itemName", item == null ? null : item.getName());
|
result.put("indicators", toRangePayload(itemId));
|
List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCode(bag.getCode());
|
MesQcSeedQualityDO quality = qualities.stream()
|
.filter(q -> !isReturnQuality(q)).findFirst().orElse(null);
|
result.put("qualityId", quality == null ? null : quality.getId());
|
result.put("result", quality == null ? null : quality.getResult());
|
result.put("qualityIds", qualities.stream().map(MesQcSeedQualityDO::getId).toList());
|
result.put("qualityLines", qualities.stream()
|
.filter(q -> !isReturnQuality(q))
|
.map(q -> {
|
Map<String, Object> line = new HashMap<>();
|
line.put("id", q.getId());
|
line.put("qcLine", q.getQcLine());
|
line.put("workflowStatus", q.getWorkflowStatus());
|
return line;
|
})
|
.toList());
|
return result;
|
}
|
|
private record Scope(String type, String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { }
|
|
/** 扫码类型:由码值自动识别袋码 / 托盘码 / 生产批次码 */
|
private static final String SCAN_TYPE_AUTO = "AUTO";
|
|
private Scope scope(String code, String type, boolean lock) {
|
// 标签二维码内容可能是整段追溯链接,先拆解为纯码值再匹配
|
code = MesTraceCodeUtils.extractCode(code);
|
if (StrUtil.isBlank(type) || SCAN_TYPE_AUTO.equalsIgnoreCase(type)) {
|
return autoScope(code, lock);
|
}
|
if ("BAG".equals(type)) {
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
|
if (bag == null) throw invalidParamException("未找到袋码,请核对扫码类型和码值");
|
return bagScope(bag);
|
}
|
if ("PALLET".equals(type)) {
|
var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
|
if (pallet == null) throw invalidParamException("未找到托盘码,请核对扫码类型和码值");
|
return palletScope(pallet, lock);
|
}
|
if ("BATCH".equals(type)) {
|
var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
|
if (batch == null) throw invalidParamException("未找到生产批次码,请核对扫码类型和码值");
|
return batchScope(batch, lock);
|
}
|
throw invalidParamException("不支持的质检扫码类型");
|
}
|
|
/** 与扫码绑定托盘一致:按码值自动识别类型,依次尝试袋码、托盘码、生产批次码 */
|
private Scope autoScope(String code, boolean lock) {
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
|
if (bag != null) {
|
return bagScope(bag);
|
}
|
var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
|
if (pallet != null) {
|
return palletScope(pallet, lock);
|
}
|
var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
|
if (batch != null) {
|
return batchScope(batch, lock);
|
}
|
throw invalidParamException("未找到对应的袋码、托盘码或生产批次码,请核对码值");
|
}
|
|
private Scope bagScope(MesProBagCodeDO bag) {
|
return new Scope("BAG", bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(),
|
Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
|
}
|
|
private Scope palletScope(MesProPalletDO pallet, boolean lock) {
|
if (pallet.getBatchId() == null) throw invalidParamException("托盘尚未绑定生产批次,无法质检");
|
return new Scope("PALLET", StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
|
Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
|
}
|
|
private Scope batchScope(MesProBatchDO batch, boolean lock) {
|
return new Scope("BATCH", StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
|
Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
|
}
|
|
@Override
|
public Map<String, Object> resolveScan(String code, String scanType) {
|
return resolveScan(code, scanType, QC_LINE_PROCESS);
|
}
|
|
@Override
|
public Map<String, Object> resolveScan(String code, String scanType, Integer qcLine) {
|
requireQcLine(qcLine);
|
Scope scope = scope(code, scanType, false);
|
List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
|
// 一次提交生成两条质检线记录,任一已存在即视为该袋已完成初检。
|
Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream()
|
.filter(q -> !isReturnQuality(q))
|
.map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
|
Map<String, Object> result = new HashMap<>();
|
// 自动识别时回传实际码类型,前端据此展示对象类型并用于提交
|
result.put("scanType", scope.type()); result.put("code", scope.code()); result.put("businessCode", scope.businessCode());
|
result.put("batchCode", scope.batchCode()); result.put("canceled", scope.canceled());
|
result.put("totalCount", effective.size());
|
result.put("inbound", !effective.isEmpty() && effective.stream().allMatch(b -> b.getInboundEventId() != null));
|
result.put("pendingBagIds", effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
|
result.put("completedBagIds", effective.stream().filter(b -> inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
|
Long itemId = resolveItemId(scope);
|
MesMdmItemDO item = itemId == null ? null : mdmItemService.getItem(itemId);
|
result.put("itemId", itemId);
|
result.put("itemCode", item == null ? null : item.getCode());
|
result.put("itemName", item == null ? null : item.getName());
|
result.put("indicators", toRangePayload(itemId));
|
if ("BAG".equals(scope.type())) {
|
List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCode(scope.code());
|
MesQcSeedQualityDO quality = qualities.stream()
|
.filter(q -> !isReturnQuality(q)).findFirst().orElse(null);
|
result.put("qualityId", quality == null ? null : quality.getId());
|
result.put("result", quality == null ? null : quality.getResult());
|
result.put("qualityIds", qualities.stream().map(MesQcSeedQualityDO::getId).toList());
|
}
|
return result;
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
|
List<Long> confirmed = batchIds(req.getBagIds(), "袋码");
|
Scope scope = scope(req.getCode(), req.getScanType(), false);
|
if (scope.canceled()) throw invalidParamException("质检对象已作废");
|
List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
|
if (effective.isEmpty()) throw new IllegalArgumentException("没有有效袋码");
|
// 一次提交同时生成两条质检线记录,任一已存在即视为该袋已质检。
|
Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream()
|
.filter(q -> !isReturnQuality(q))
|
.map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
|
List<MesProBagCodeDO> pendingPreview = effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode()))
|
.sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
|
List<Long> expectedChunk = pendingPreview.stream().limit(MAX_BATCH_SIZE).map(MesProBagCodeDO::getId).toList();
|
if (expectedChunk.isEmpty() || !confirmed.equals(expectedChunk))
|
throw new IllegalArgumentException("待质检清单已变化或已全部质检,请重新扫描,禁止重复处理");
|
|
List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(confirmed);
|
if (locked.size() != confirmed.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
|
throw new IllegalArgumentException("袋码已变化或尚未全部入库,请重新扫描核对");
|
// 在袋码锁之后重新核对归属;其他绑定操作不得改变已确认的检验范围。
|
Scope current = scope(req.getCode(), req.getScanType(), false);
|
Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
|
if (!currentIds.containsAll(confirmed))
|
throw new IllegalArgumentException("袋码归属已变化,请重新扫描核对");
|
if (qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream()
|
.anyMatch(q -> !isReturnQuality(q)))
|
throw new IllegalArgumentException("待质检清单已变化或已全部质检,请重新扫描,禁止重复处理");
|
List<MesProBagCodeDO> pending = locked.stream().sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
|
submitInitialBatch(pending, req);
|
return Map.of("processedCount", pending.size());
|
}
|
|
private void submitInitialBatch(List<MesProBagCodeDO> bags,
|
cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
|
Long batchId = bags.get(0).getBatchId();
|
if (batchId == null || bags.stream().anyMatch(bag -> !Objects.equals(batchId, bag.getBatchId())))
|
throw new IllegalArgumentException("批量初检只允许同一生产批次的袋码");
|
MesProBatchDO batch = proBatchService.getBatch(batchId);
|
if (batch == null || batch.getItemId() == null) throw new IllegalArgumentException("未根据袋码解析到物料信息");
|
Map<String, IndicatorMeta> expected = requireEffectiveIndicators(batch.getItemId());
|
boolean passed = validateIndicators(req.getIndicators(), expected);
|
if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
|
if (Integer.valueOf(40).equals(batch.getStatus())) throw new IllegalArgumentException("生产批次已作废,无法质检");
|
MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
|
if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0)
|
throw new IllegalArgumentException("该批次库存不在暂存库中,无法进行质检");
|
if (warehouseService.getFirstQualifiedWarehouse() == null) throw exception(WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED);
|
MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(req.getQualifiedWarehouseId());
|
if (qualifiedWarehouse == null) throw exception(WM_WAREHOUSE_NOT_EXISTS);
|
if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType()))
|
throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
|
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
int result = passed ? 1 : 2;
|
int status = passed ? WAIT_PROCESS : WAIT_RETEST;
|
// 一次提交同时生成加工部与销售部两条记录,指标与结论完全一致。
|
List<MesQcSeedQualityDO> qualities = new ArrayList<>(bags.size() * 2);
|
for (MesProBagCodeDO bag : bags) {
|
for (int qcLine : new int[]{QC_LINE_PROCESS, QC_LINE_SALES}) {
|
qualities.add(MesQcSeedQualityDO.builder()
|
.code(bag.getCode()).qcLine(qcLine).bizType(BIZ_TYPE_PRODUCTION)
|
.itemId(batch.getItemId()).batchId(batch.getId()).batchCode(batch.getCode())
|
.result(result).reason(passed ? "" : req.getReason().trim()).workflowStatus(status)
|
.qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId).inspectedAt(now).build());
|
}
|
}
|
qualityMapper.insertBatch(qualities, WRITE_BATCH_SIZE);
|
if (qualities.stream().anyMatch(quality -> quality.getId() == null))
|
throw new IllegalStateException("批量初检记录编号生成失败");
|
|
List<MesQcSeedQualityIndicatorDO> indicators = new ArrayList<>(qualities.size() * expected.size());
|
List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size());
|
for (MesQcSeedQualityDO quality : qualities) {
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
|
IndicatorMeta meta = expected.get(item.getCode());
|
indicators.add(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
|
.indicatorName(meta.name()).unit(meta.unit())
|
.value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
|
}
|
actions.add(newAction(quality.getId(), "INITIAL", result, req.getReason(), req.getIndicators(), operatorId, now));
|
}
|
indicatorMapper.insertBatch(indicators, WRITE_BATCH_SIZE * expected.size());
|
actionMapper.insertBatch(actions, WRITE_BATCH_SIZE);
|
String batchCode = batch.getCode();
|
if (passed) {
|
afterCommit(() -> startLineProcess(QC_LINE_PROCESS, batchCode, operatorId));
|
afterCommit(() -> startLineProcess(QC_LINE_SALES, batchCode, operatorId));
|
}
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
|
// 种子质检必须以袋码(单袋)提交,物料信息由袋码自动解析,前端不再单独选择物料
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(reqVO.getCode());
|
if (bag == null) {
|
throw new IllegalArgumentException("未解析到有效袋码,种子质检必须以袋码提交");
|
}
|
if (Integer.valueOf(30).equals(bag.getStatus()) || bag.getInboundEventId() == null)
|
throw new IllegalArgumentException("袋码已作废或尚未入库,无法质检");
|
// 双码锁定同一袋,并以追溯码记录结果,防止换码或并发重复扣减库存。
|
if (qualityMapper.selectListByCodeForUpdate(bag.getCode()).stream()
|
.anyMatch(q -> !isReturnQuality(q)))
|
throw new IllegalArgumentException("该袋码已质检,禁止重复提交");
|
MesProBatchDO batch = bag.getBatchId() == null ? null : proBatchService.getBatch(bag.getBatchId());
|
if (batch == null || batch.getItemId() == null) {
|
throw new IllegalArgumentException("未根据袋码解析到物料信息,请确认扫码内容或批次号正确");
|
}
|
if (Integer.valueOf(40).equals(batch.getStatus())) 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);
|
}
|
Map<String, IndicatorMeta> expected = requireEffectiveIndicators(batch.getItemId());
|
boolean passed = validateIndicators(reqVO.getIndicators(), expected);
|
if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
int result = passed ? 1 : 2;
|
int status = passed ? WAIT_PROCESS : WAIT_RETEST;
|
// 一次提交同时生成加工部与销售部两条记录,指标与结论完全一致。
|
Long firstQualityId = null;
|
for (int qcLine : new int[]{QC_LINE_PROCESS, QC_LINE_SALES}) {
|
MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode())
|
.qcLine(qcLine).bizType(BIZ_TYPE_PRODUCTION)
|
.itemId(batch.getItemId()).batchCode(batch.getCode()).batchId(batch.getId())
|
.reason(passed ? "" : reqVO.getReason().trim()).result(result).workflowStatus(status)
|
.qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId)
|
.inspectedAt(now).build();
|
qualityMapper.insert(quality);
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
|
IndicatorMeta meta = expected.get(item.getCode());
|
indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
|
.indicatorName(meta.name()).unit(meta.unit())
|
.value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
|
}
|
recordAction(quality.getId(), "INITIAL", result, reqVO.getReason(), reqVO.getIndicators());
|
if (firstQualityId == null) firstQualityId = quality.getId();
|
}
|
// 初检仅记录结论;袋仍留在暂存库,待初检审核、复检及最终审核后再放行或报废。
|
String batchCode = batch.getCode();
|
if (passed) {
|
afterCommit(() -> startLineProcess(QC_LINE_PROCESS, batchCode, operatorId));
|
afterCommit(() -> startLineProcess(QC_LINE_SALES, batchCode, operatorId));
|
}
|
return firstQualityId;
|
}
|
|
private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) {
|
MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(quality.getCode());
|
if (bag == null || bag.getInboundEventId() == null || !Objects.equals(bag.getQcStatus(), MesProBagCodeQcStatusEnum.UNCHECKED.getStatus()))
|
throw new IllegalArgumentException("袋码状态已变化,禁止重复库存流转");
|
MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
|
if (batch == null) throw new IllegalArgumentException("袋码所属生产批次不存在");
|
MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
|
if (wmBatch == null) throw new IllegalArgumentException("暂存库存所属仓库批次不存在");
|
MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(quality.getQualifiedWarehouseId());
|
if (passed && (qualifiedWarehouse == null || Boolean.TRUE.equals(qualifiedWarehouse.getFrozen())
|
|| !MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())))
|
throw new IllegalArgumentException("目标合格库不存在或类型已变化");
|
Long qualifiedLocationId = null;
|
Long qualifiedAreaId = null;
|
if (passed) {
|
// 库存事务必须落到库区、库位,合格库缺少配置时提前给出可操作的提示
|
Long[] target = resolveQualifiedLocation(qualifiedWarehouse);
|
qualifiedLocationId = target[0];
|
qualifiedAreaId = target[1];
|
}
|
// 按袋码粒度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减
|
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(quality.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(quality.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("[finalizeStock][审批放行,袋码 {} 由暂存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName());
|
} else {
|
// C. 质检不合格 → 该袋报废;暂存出库事务已扣减暂存库存,标记袋码报废
|
materialStockBagService.scrap(tempOutReq, tempOutTxId);
|
bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus());
|
log.info("[finalizeStock][审批报废,袋码 {}]", bag.getCode());
|
}
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
|
MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
|
if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
|
throw new IllegalArgumentException("当前状态不能提交复检,请刷新后重试");
|
boolean returnQuality = Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN);
|
if (returnQuality && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST))
|
throw new IllegalArgumentException("经销商退货质检当前状态不能复检,请刷新后重试");
|
boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK);
|
if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("检验袋码与原质检记录不一致");
|
// 复检沿用原记录的指标集合,避免配置变更导致历史记录无法复检
|
Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>();
|
for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id))
|
existing.put(indicator.getIndicatorCode(), indicator);
|
boolean passed = validateIndicators(req.getIndicators(), toMetaMap(existing.values(), quality.getItemId()));
|
if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("检验不合格必须填写原因");
|
recordAction(id, initialRecheck ? "INITIAL_RECHECK" : "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
|
Long operatorId = getLoginUserId();
|
LocalDateTime now = LocalDateTime.now();
|
int nextStatus;
|
if (returnQuality) {
|
// 复检结论统一交由销售部领导审批,不合格时审批后不予退货。
|
nextStatus = WAIT_PROCESS;
|
} else if (initialRecheck) {
|
nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW;
|
} else {
|
nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW;
|
}
|
MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
|
.reason(passed ? "" : req.getReason().trim())
|
.inspectorId(operatorId).inspectedAt(now)
|
.workflowStatus(nextStatus).build();
|
if (!initialRecheck) {
|
update.setRetestResult(passed ? 1 : 2);
|
update.setRetestedAt(now);
|
}
|
qualityMapper.updateById(update);
|
if (returnQuality) {
|
afterCommit(() -> startReturnProcess(quality, operatorId));
|
} else if (initialRecheck) {
|
afterCommit(() -> startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId));
|
} else if (!initialRecheck) {
|
// 初检不合格的复检完成后,先交本线部门审批。
|
afterCommit(() -> startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId));
|
}
|
// 页面、公开追溯读取主指标表;同步为最新一次测值,历次测值在操作留痕表保留。
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
|
MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
|
if (indicator == null) throw new IllegalArgumentException("质检指标记录缺失");
|
indicatorMapper.updateById(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
|
.value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
|
}
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO req) {
|
List<Long> ids = approvalIds(req.getIds());
|
List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(ids);
|
validateLockedIds(ids, qualities);
|
String batchCode = null;
|
Integer workflowStatus = null;
|
Integer batchQcLine = null;
|
boolean first = true;
|
for (MesQcSeedQualityDO quality : qualities) {
|
if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
|
throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试");
|
if (StrUtil.isBlank(quality.getBatchCode()))
|
throw new IllegalArgumentException("记录缺少生产批次,不能批量质检");
|
if (first) {
|
batchCode = quality.getBatchCode();
|
workflowStatus = quality.getWorkflowStatus();
|
batchQcLine = quality.getQcLine();
|
first = false;
|
} else if (!Objects.equals(batchCode, quality.getBatchCode())
|
|| !Objects.equals(workflowStatus, quality.getWorkflowStatus())
|
|| !Objects.equals(batchQcLine, quality.getQcLine())) {
|
throw new IllegalArgumentException("批量质检只允许同生产批次、同质检线和同流程状态的记录");
|
}
|
}
|
applyRetestBatch(qualities, req.getIndicators(), req.getReason());
|
}
|
|
private void checkRetestScanType(String scanType) {
|
if (!Set.of("PALLET", "BATCH").contains(scanType))
|
throw new IllegalArgumentException("批量质检只支持托盘码或生产批次码");
|
}
|
|
@Override
|
public Map<String, Object> resolveRetestScope(String code, String scanType) {
|
return resolveRetestScope(code, scanType, QC_LINE_PROCESS);
|
}
|
|
@Override
|
public Map<String, Object> resolveRetestScope(String code, String scanType, Integer qcLine) {
|
requireQcLine(qcLine);
|
checkRetestScanType(scanType);
|
Scope scope = scope(code, scanType, false);
|
if (scope.canceled()) throw invalidParamException("质检对象已作废");
|
List<String> codes = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
|
.map(MesProBagCodeDO::getCode).toList();
|
List<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes).stream()
|
.filter(q -> Objects.equals(q.getQcLine(), qcLine) && !isReturnQuality(q)).toList();
|
List<Long> retestIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_RETEST))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<Long> initialRecheckIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_RECHECK))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<Long> pendingProcessIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<Long> pendingInitialFailIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<Long> pendingCompanyIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<Long> pendingCompanyFailIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL))
|
.map(MesQcSeedQualityDO::getId).sorted().toList();
|
Map<String, Object> result = new HashMap<>();
|
result.put("code", scope.code()); result.put("scanType", scanType); result.put("batchCode", scope.batchCode());
|
result.put("totalCount", codes.size()); result.put("retestIds", retestIds);
|
result.put("initialRecheckIds", initialRecheckIds);
|
result.put("pendingProcessIds", pendingProcessIds); result.put("pendingInitialFailIds", pendingInitialFailIds);
|
result.put("pendingCompanyIds", pendingCompanyIds); result.put("pendingCompanyFailIds", pendingCompanyFailIds);
|
result.put("pendingProcessCount", pendingProcessIds.size() + pendingInitialFailIds.size());
|
result.put("pendingCompanyCount", pendingCompanyIds.size() + pendingCompanyFailIds.size());
|
Long itemId = qualities.stream().map(MesQcSeedQualityDO::getItemId).filter(Objects::nonNull).findFirst().orElse(null);
|
result.put("itemId", itemId);
|
MesQcSeedQualityDO actionable = qualities.stream()
|
.filter(q -> Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(q.getWorkflowStatus()))
|
.findFirst().orElse(null);
|
result.put("indicators", actionable == null ? List.of()
|
: toIndicatorPayload(indicatorMapper.selectListByQualityId(actionable.getId()), itemId));
|
return result;
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO req) {
|
requireQcLine(req.getQcLine());
|
checkRetestScanType(req.getScanType());
|
if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus()))
|
throw new IllegalArgumentException("批量质检状态无效");
|
List<Long> confirmed = approvalIds(req.getQualityIds());
|
Scope scope = scope(req.getCode(), req.getScanType(), false);
|
if (scope.canceled()) throw invalidParamException("质检对象已作废");
|
List<MesProBagCodeDO> effective = scope.bags().stream()
|
.filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null).toList();
|
if (effective.isEmpty()) throw new IllegalArgumentException("当前范围没有已入库的有效袋码");
|
Set<String> effectiveCodes = effective.stream().map(MesProBagCodeDO::getCode).collect(java.util.stream.Collectors.toSet());
|
List<MesQcSeedQualityDO> pendingPreview = qualityMapper.selectListByCodes(effectiveCodes).stream()
|
.filter(q -> Objects.equals(q.getQcLine(), req.getQcLine())
|
&& !isReturnQuality(q) && Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus()))
|
.sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList();
|
List<Long> expectedChunk = pendingPreview.stream().limit(MAX_BATCH_SIZE).map(MesQcSeedQualityDO::getId).toList();
|
if (expectedChunk.isEmpty() || !confirmed.equals(expectedChunk))
|
throw new IllegalArgumentException("待质检清单已变化,请重新扫描核对");
|
List<MesQcSeedQualityDO> pending = qualityMapper.selectListByIdsForUpdate(confirmed);
|
validateLockedIds(confirmed, pending);
|
if (pending.stream().anyMatch(quality -> !Objects.equals(quality.getWorkflowStatus(), req.getWorkflowStatus())
|
|| !Objects.equals(quality.getQcLine(), req.getQcLine()) || isReturnQuality(quality)
|
|| !effectiveCodes.contains(quality.getCode())))
|
throw new IllegalArgumentException("待质检记录状态或归属已变化,请重新扫描核对");
|
applyRetestBatch(pending, req.getIndicators(), req.getReason());
|
return Map.of("processedCount", pending.size());
|
}
|
|
private void applyRetestBatch(List<MesQcSeedQualityDO> qualities,
|
List<MesQcSeedQualitySubmitReqVO.Indicator> submittedIndicators, String reason) {
|
List<Long> ids = qualities.stream().map(MesQcSeedQualityDO::getId).toList();
|
Map<Long, Map<String, MesQcSeedQualityIndicatorDO>> existingByQuality = new HashMap<>();
|
for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityIds(ids)) {
|
existingByQuality.computeIfAbsent(indicator.getQualityId(), ignored -> new HashMap<>())
|
.put(indicator.getIndicatorCode(), indicator);
|
}
|
Map<String, MesQcSeedQualityIndicatorDO> firstIndicators = existingByQuality.get(qualities.get(0).getId());
|
Map<String, IndicatorMeta> expected = toMetaMap(
|
firstIndicators == null ? Collections.<MesQcSeedQualityIndicatorDO>emptyList() : firstIndicators.values(),
|
qualities.get(0).getItemId());
|
boolean passed = validateIndicators(submittedIndicators, expected);
|
if (!passed && StrUtil.isBlank(reason)) throw new IllegalArgumentException("检验不合格必须填写原因");
|
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
int result = passed ? 1 : 2;
|
List<MesQcSeedQualityDO> qualityUpdates = new ArrayList<>(qualities.size());
|
List<MesQcSeedQualityIndicatorDO> indicatorUpdates = new ArrayList<>(qualities.size() * submittedIndicators.size());
|
List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size());
|
Set<String> lineProcessTargets = new HashSet<>();
|
List<MesQcSeedQualityDO> returnReady = new ArrayList<>();
|
for (MesQcSeedQualityDO quality : qualities) {
|
boolean returnQuality = Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN);
|
boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK);
|
if (returnQuality && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST))
|
throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试");
|
if (!returnQuality && !initialRecheck && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST))
|
throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试");
|
int nextStatus;
|
if (returnQuality) {
|
nextStatus = WAIT_PROCESS;
|
returnReady.add(quality);
|
} else if (initialRecheck) {
|
nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW;
|
lineProcessTargets.add(quality.getQcLine() + "#" + quality.getBatchCode());
|
} else {
|
nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW;
|
lineProcessTargets.add(quality.getQcLine() + "#" + quality.getBatchCode());
|
}
|
MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(quality.getId()).result(result)
|
.reason(passed ? "" : reason.trim()).inspectorId(operatorId).inspectedAt(now)
|
.workflowStatus(nextStatus).build();
|
if (!initialRecheck) {
|
update.setRetestResult(result);
|
update.setRetestedAt(now);
|
}
|
qualityUpdates.add(update);
|
Map<String, MesQcSeedQualityIndicatorDO> existing = existingByQuality.getOrDefault(quality.getId(), Map.of());
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : submittedIndicators) {
|
MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
|
if (indicator == null) throw new IllegalArgumentException("质检指标记录缺失");
|
indicatorUpdates.add(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
|
.value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
|
}
|
actions.add(newAction(quality.getId(), initialRecheck ? "INITIAL_RECHECK" : "RETEST", result,
|
reason, submittedIndicators, operatorId, now));
|
}
|
qualityMapper.updateBatch(qualityUpdates, WRITE_BATCH_SIZE);
|
indicatorMapper.updateBatch(indicatorUpdates, WRITE_BATCH_SIZE * submittedIndicators.size());
|
actionMapper.insertBatch(actions, WRITE_BATCH_SIZE);
|
// 复检按记录所属质检线分别发起本线审批,避免提前触发另一条线。
|
lineProcessTargets.forEach(target -> {
|
String[] parts = target.split("#", 2);
|
afterCommit(() -> startLineProcess(Integer.valueOf(parts[0]), parts[1], operatorId));
|
});
|
returnReady.forEach(quality -> afterCommit(() -> startReturnProcess(quality, operatorId)));
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void processApprove(Long id, boolean approved, String opinion) {
|
MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
|
if (quality == null || !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))
|
throw new IllegalArgumentException("当前状态不能进行初检审核");
|
requireLegacyApproval(quality);
|
applyProcessApproval(quality, approved, opinion, true);
|
}
|
|
/**
|
* 本线领导审批:生产质检通过后进入公司质检审批,驳回后重新初检;经销商退货质检则直接终审。
|
*/
|
private void applyProcessApproval(MesQcSeedQualityDO quality, boolean approved, String opinion, boolean validateApprover) {
|
if (validateApprover) {
|
approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
|
MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
|
}
|
if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) {
|
if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS))
|
throw new IllegalArgumentException("经销商退货质检当前状态不能进行销售部审批");
|
applyReturnApproval(quality, approved, opinion, operatorId, now);
|
return;
|
}
|
int next = approved ? (Objects.equals(quality.getQcLine(), 0) ? WAIT_RETEST
|
: Objects.equals(quality.getResult(), 1) ? WAIT_COMPANY : WAIT_COMPANY_FAIL)
|
: WAIT_INITIAL_RECHECK;
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()).workflowStatus(next)
|
.processApprovedAt(now).build());
|
if (!approved) qualityMapper.clearLineProcessInstance(quality.getId());
|
actionMapper.insert(newAction(quality.getId(), approved ? "PROCESS_APPROVE" : "PROCESS_REJECT",
|
quality.getResult(), opinion, null, operatorId, now));
|
if (approved && !Objects.equals(quality.getQcLine(), 0))
|
afterCommit(() -> maybeStartCompanyProcess(quality.getBatchCode(), operatorId));
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void companyApprove(Long id, boolean approved, String opinion) {
|
MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
|
if (quality == null) throw new IllegalArgumentException("质检记录不存在");
|
if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN))
|
throw new IllegalArgumentException("经销商退货质检请使用销售部审批");
|
if (!Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
|
throw new IllegalArgumentException("当前状态不能进行复检审核");
|
requireLegacyApproval(quality);
|
approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
|
MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId());
|
applyCompanyApproval(quality.getCode(), approved, opinion, getLoginUserId());
|
}
|
|
/**
|
* 公司质检审批:以「袋」为单位成对审批,两条质检线都走完才允许审批。
|
* 两条线均合格才放行入库,任一条线不合格则整袋报废;驳回则两条线全部退回复检。
|
*/
|
private void applyCompanyApproval(String code, boolean approved, String opinion, Long operatorId) {
|
if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
|
List<MesQcSeedQualityDO> pair = qualityMapper.selectListByCodeForUpdate(code).stream()
|
.filter(q -> !isReturnQuality(q)).toList();
|
if (pair.size() != 2 || pair.stream().anyMatch(q -> !Objects.equals(q.getQcLine(), QC_LINE_PROCESS)
|
&& !Objects.equals(q.getQcLine(), QC_LINE_SALES)))
|
throw new IllegalArgumentException("袋码 " + code + " 的双线质检记录不完整,无法公司审批");
|
if (pair.stream().anyMatch(q -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(q.getWorkflowStatus())))
|
throw new IllegalArgumentException("袋码 " + code + " 的双线质检尚未全部完成,不能进行公司质检审批");
|
boolean passed = pair.stream().allMatch(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY));
|
MesQcSeedQualityDO representative = pair.stream()
|
.filter(q -> Objects.equals(q.getQcLine(), QC_LINE_PROCESS)).findFirst().orElse(pair.get(0));
|
LocalDateTime now = LocalDateTime.now();
|
if (approved) {
|
finalizeStock(representative, passed);
|
qualityMapper.updateBatch(pair.stream().map(quality -> MesQcSeedQualityDO.builder()
|
.id(quality.getId()).workflowStatus(passed ? RELEASED : SCRAPPED)
|
.companyApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE);
|
} else {
|
qualityMapper.clearCompanyProcessInstance(code);
|
pair.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId()));
|
qualityMapper.updateBatch(pair.stream().map(quality -> MesQcSeedQualityDO.builder()
|
.id(quality.getId()).workflowStatus(WAIT_RETEST).companyApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE);
|
}
|
actionMapper.insertBatch(pair.stream().map(quality -> newAction(quality.getId(),
|
approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(),
|
WRITE_BATCH_SIZE);
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void processApproveBatch(List<Long> ids, boolean approved, String opinion) {
|
List<Long> confirmed = approvalIds(ids);
|
List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed);
|
validateLockedIds(confirmed, qualities);
|
qualities.forEach(MesQcSeedQualityServiceImpl::requireLegacyApproval);
|
if (qualities.stream().anyMatch(quality -> !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())))
|
throw new IllegalArgumentException("所选记录包含不能进行初检审核的状态,请刷新后重试");
|
approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
|
MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
|
if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
List<MesQcSeedQualityDO> production = qualities.stream()
|
.filter(quality -> !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)).toList();
|
qualities.stream().filter(quality -> Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN))
|
.forEach(quality -> applyProcessApproval(quality, approved, opinion, false));
|
if (production.isEmpty()) return;
|
int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK;
|
qualityMapper.updateBatch(production.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId())
|
.workflowStatus(next)
|
.processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE);
|
if (!approved) production.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId()));
|
actionMapper.insertBatch(production.stream().map(quality -> newAction(quality.getId(),
|
approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(),
|
WRITE_BATCH_SIZE);
|
}
|
|
@Override @Transactional(rollbackFor = Exception.class)
|
public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) {
|
List<Long> confirmed = approvalIds(ids);
|
List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed);
|
validateLockedIds(confirmed, qualities);
|
qualities.forEach(MesQcSeedQualityServiceImpl::requireLegacyApproval);
|
if (qualities.stream().anyMatch(quality -> Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)))
|
throw new IllegalArgumentException("经销商退货质检请使用销售部审批");
|
approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
|
MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId());
|
if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
|
Long operatorId = getLoginUserId();
|
// 公司审批以袋为单位成对处理,同一袋的两条线记录一次审批
|
qualities.stream().map(MesQcSeedQualityDO::getCode).distinct().sorted()
|
.forEach(code -> applyCompanyApproval(code, approved, opinion, operatorId));
|
}
|
|
// ==================== 经销商退货质检 ====================
|
|
@Override
|
public Map<String, Object> resolveReturnQuality(Long returnSalesId) {
|
MesWmReturnSalesDO returnSales = returnSalesService.getReturnSales(returnSalesId);
|
if (returnSales == null) throw new IllegalArgumentException("销售退货单不存在");
|
List<MesWmReturnSalesLineDO> lines = returnSalesLineService.getReturnSalesLineListByReturnId(returnSalesId);
|
MesWmReturnSalesLineDO first = lines.stream().filter(line -> line.getItemId() != null).findFirst().orElse(null);
|
Long itemId = first == null ? null : first.getItemId();
|
MesQcSeedQualityDO quality = qualityMapper.selectByReturnSalesId(returnSalesId);
|
Map<String, Object> result = new HashMap<>();
|
result.put("returnSalesId", returnSales.getId());
|
result.put("returnSalesCode", returnSales.getCode());
|
result.put("returnSalesStatus", returnSales.getStatus());
|
result.put("itemId", itemId);
|
result.put("batchId", first == null ? null : first.getBatchId());
|
result.put("batchCode", first == null ? null : first.getBatchCode());
|
result.put("indicators", toRangePayload(itemId));
|
result.put("qualityId", quality == null ? null : quality.getId());
|
result.put("result", quality == null ? null : quality.getResult());
|
result.put("workflowStatus", quality == null ? null : quality.getWorkflowStatus());
|
return result;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long submitReturnQuality(MesQcSeedReturnQualitySubmitReqVO reqVO) {
|
MesWmReturnSalesDO returnSales = returnSalesService.getReturnSales(reqVO.getReturnSalesId());
|
if (returnSales == null) throw new IllegalArgumentException("销售退货单不存在");
|
if (qualityMapper.selectByReturnSalesId(returnSales.getId()) != null)
|
throw new IllegalArgumentException("该销售退货单已完成退货质检,禁止重复提交");
|
if (!Set.of(MesWmReturnSalesStatusEnum.PREPARE.getStatus(),
|
MesWmReturnSalesStatusEnum.CONFIRMED.getStatus()).contains(returnSales.getStatus()))
|
throw new IllegalArgumentException("当前销售退货单状态不能进行退货质检");
|
List<MesWmReturnSalesLineDO> lines = returnSalesLineService.getReturnSalesLineListByReturnId(returnSales.getId());
|
if (CollUtil.isEmpty(lines)) throw new IllegalArgumentException("销售退货单没有明细,无法质检");
|
MesWmReturnSalesLineDO first = lines.stream().filter(line -> line.getItemId() != null).findFirst()
|
.orElseThrow(() -> new IllegalArgumentException("销售退货单明细缺少物料,无法质检"));
|
Map<String, IndicatorMeta> expected = requireEffectiveIndicators(first.getItemId());
|
boolean passed = validateIndicators(reqVO.getIndicators(), expected);
|
if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
|
LocalDateTime now = LocalDateTime.now();
|
Long operatorId = getLoginUserId();
|
int result = passed ? 1 : 2;
|
MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder()
|
.code(returnSales.getCode()).qcLine(QC_LINE_SALES).bizType(BIZ_TYPE_RETURN)
|
.itemId(first.getItemId()).batchId(first.getBatchId()).batchCode(first.getBatchCode())
|
.result(result).reason(passed ? "" : reqVO.getReason().trim())
|
// 初检合格进入销售部审批;不合格先复检。
|
.workflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST)
|
.returnSalesId(returnSales.getId()).returnSalesCode(returnSales.getCode())
|
.inspectorId(operatorId).inspectedAt(now).build();
|
qualityMapper.insert(quality);
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
|
IndicatorMeta meta = expected.get(item.getCode());
|
indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId())
|
.indicatorCode(item.getCode()).indicatorName(meta.name()).unit(meta.unit())
|
.value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
|
}
|
recordAction(quality.getId(), "INITIAL", result, reqVO.getReason(), reqVO.getIndicators());
|
returnSalesService.updateReturnSalesStatus(returnSales.getId(), MesWmReturnSalesStatusEnum.CONFIRMED.getStatus());
|
if (passed) afterCommit(() -> startReturnProcess(quality, operatorId));
|
return quality.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void returnApprove(Long id, boolean approved, String opinion) {
|
MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
|
if (quality == null || !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN))
|
throw new IllegalArgumentException("质检记录不存在或不是经销商退货质检");
|
requireLegacyApproval(quality);
|
if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS))
|
throw new IllegalArgumentException("当前状态不能进行销售部审批");
|
approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
|
MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
|
applyReturnApproval(quality, approved, opinion, getLoginUserId(), LocalDateTime.now());
|
}
|
|
/**
|
* 销售部领导审批:通过则退货单进入执行;不通过则不予退货并取消退货单,质检记录留存。
|
*/
|
private void applyReturnApproval(MesQcSeedQualityDO quality, boolean approved, String opinion,
|
Long operatorId, LocalDateTime now) {
|
if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
|
if (approved && Objects.equals(quality.getResult(), 1)) {
|
returnSalesService.updateReturnSalesStatus(quality.getReturnSalesId(),
|
MesWmReturnSalesStatusEnum.APPROVING.getStatus());
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId())
|
.workflowStatus(RELEASED).processApprovedAt(now).build());
|
} else {
|
cancelReturnSalesQuietly(quality.getReturnSalesId());
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId())
|
.workflowStatus(SCRAPPED).processApprovedAt(now).build());
|
}
|
actionMapper.insert(newAction(quality.getId(), approved ? "RETURN_APPROVE" : "RETURN_REJECT",
|
quality.getResult(), opinion, null, operatorId, now));
|
}
|
|
// ==================== BPM 流程回调 ====================
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void handleLineProcessResult(String processInstanceId, Integer qcLine, String batchCode, boolean approved, String reason) {
|
if (StrUtil.isBlank(processInstanceId) || qcLine == null || StrUtil.isBlank(batchCode)) return;
|
List<MesQcSeedQualityDO> pending = qualityMapper.selectListByProcessInstanceId(processInstanceId).stream()
|
.filter(quality -> Objects.equals(quality.getQcLine(), qcLine)
|
&& Objects.equals(quality.getBatchCode(), batchCode)
|
&& !isReturnQuality(quality)
|
&& Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))
|
.toList();
|
if (pending.isEmpty()) {
|
log.info("[handleLineProcessResult][批次 {} 质检线 {} 无待审批记录,忽略 BPM 事件]", batchCode, qcLine);
|
return;
|
}
|
List<Long> ids = pending.stream().map(MesQcSeedQualityDO::getId).sorted().toList();
|
List<MesQcSeedQualityDO> locked = qualityMapper.selectListByIdsForUpdate(ids);
|
validateLockedIds(ids, locked);
|
LocalDateTime now = LocalDateTime.now();
|
int next = approved ? WAIT_COMPANY : WAIT_INITIAL_RECHECK;
|
String finalReason = StrUtil.blankToDefault(reason, approved ? "审批通过" : "审批驳回");
|
qualityMapper.updateBatch(locked.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId())
|
.workflowStatus(approved && Objects.equals(quality.getResult(), 2) ? WAIT_COMPANY_FAIL : next)
|
.processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE);
|
if (!approved) locked.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId()));
|
actionMapper.insertBatch(locked.stream().map(quality -> newAction(quality.getId(),
|
approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), finalReason, null, null, now)).toList(),
|
WRITE_BATCH_SIZE);
|
if (approved) {
|
Long inspectorId = locked.stream().map(MesQcSeedQualityDO::getInspectorId)
|
.filter(Objects::nonNull).findFirst().orElse(null);
|
afterCommit(() -> maybeStartCompanyProcess(batchCode, inspectorId));
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void handleCompanyProcessResult(String processInstanceId, String businessKey, boolean approved, String reason) {
|
if (StrUtil.isBlank(processInstanceId)) return;
|
// 公司审批按生产批次发起,一个流程实例覆盖该批次的所有袋;
|
// 按流程实例编号回查可同时兼容历史按袋码发起的审批。
|
List<MesQcSeedQualityDO> pending = qualityMapper.selectListByCompanyProcessInstanceId(processInstanceId).stream()
|
.filter(quality -> !isReturnQuality(quality)
|
&& Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
|
.toList();
|
if (pending.isEmpty()) {
|
log.info("[handleCompanyProcessResult][流程 {} 无待公司审批记录,忽略 BPM 事件]", processInstanceId);
|
return;
|
}
|
String finalReason = StrUtil.blankToDefault(reason, approved ? "公司质检审批通过" : "公司质检审批驳回");
|
// 放行/报废以袋为单位落库,同一批次的多个袋依次处理
|
pending.stream().map(MesQcSeedQualityDO::getCode).distinct().sorted()
|
.forEach(code -> applyCompanyApproval(code, approved, finalReason, null));
|
log.info("[handleCompanyProcessResult][公司质检审批 {} 完成,批次/袋码 {} 共处理 {} 条记录]",
|
processInstanceId, businessKey, pending.size());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void handleReturnProcessResult(String processInstanceId, Long returnSalesId, boolean approved, String reason) {
|
if (StrUtil.isBlank(processInstanceId) || returnSalesId == null) return;
|
MesQcSeedQualityDO quality = qualityMapper.selectByReturnSalesIdForUpdate(returnSalesId);
|
if (quality == null || !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)
|
|| !Objects.equals(quality.getProcessInstanceId(), processInstanceId)
|
|| !Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS)) {
|
log.info("[handleReturnProcessResult][销售退货单 {} 无待审批退货质检,忽略 BPM 事件]", returnSalesId);
|
return;
|
}
|
applyReturnApproval(quality, approved, StrUtil.blankToDefault(reason, approved ? "审批通过" : "审批驳回"),
|
null, LocalDateTime.now());
|
}
|
|
private void startLineProcess(Integer qcLine, String batchCode, Long operatorId) {
|
if (qcLine == null || StrUtil.isBlank(batchCode) || operatorId == null) return;
|
List<MesQcSeedQualityDO> pending = qualityMapper.selectListByBatchCode(batchCode).stream()
|
.filter(quality -> Objects.equals(quality.getQcLine(), qcLine)
|
&& !isReturnQuality(quality)
|
&& StrUtil.isBlank(quality.getProcessInstanceId())
|
&& Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))
|
.toList();
|
if (pending.isEmpty()) return;
|
Map<String, Object> variables = new HashMap<>();
|
variables.put("qcLine", qcLine);
|
variables.put("batchCode", batchCode);
|
String processDefinitionKey = Objects.equals(qcLine, QC_LINE_SALES)
|
? SALES_LINE_PROCESS_KEY : LINE_PROCESS_KEY;
|
String processInstanceId = processInstanceApi.createProcessInstance(operatorId,
|
new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(processDefinitionKey)
|
.setBusinessKey(qcLine + "#" + batchCode)
|
.setVariables(variables));
|
for (MesQcSeedQualityDO quality : pending) {
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId())
|
.processInstanceId(processInstanceId).build());
|
}
|
log.info("[startLineProcess][批次 {} 质检线 {} 已发起本线领导审批 {}", batchCode, qcLine, processInstanceId);
|
}
|
|
/**
|
* 双线部门审批全部完成后,按生产批次统一发起一次公司质检审批。
|
* 同一批次的多袋共用同一个流程实例,避免每袋各发一条审批。
|
*/
|
private void maybeStartCompanyProcess(String batchCode, Long operatorId) {
|
if (StrUtil.isBlank(batchCode) || operatorId == null) return;
|
List<MesQcSeedQualityDO> records = qualityMapper.selectListByBatchCode(batchCode).stream()
|
.filter(quality -> !isReturnQuality(quality))
|
// 已放行/已报废的记录属于已终结的批次,不再参与新的公司审批
|
.filter(quality -> !Set.of(RELEASED, SCRAPPED).contains(quality.getWorkflowStatus()))
|
.toList();
|
if (records.isEmpty()) return;
|
if (records.stream().anyMatch(quality -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL)
|
.contains(quality.getWorkflowStatus()))) return;
|
if (records.stream().anyMatch(quality -> StrUtil.isNotBlank(quality.getCompanyProcessInstanceId()))) return;
|
Map<String, Object> variables = new HashMap<>();
|
variables.put("batchCode", batchCode);
|
// 兼容流程条件中引用的 code 变量
|
variables.put("code", batchCode);
|
variables.put("passed", records.stream().allMatch(quality -> Objects.equals(quality.getWorkflowStatus(), WAIT_COMPANY)));
|
String processInstanceId = processInstanceApi.createProcessInstance(operatorId,
|
new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(COMPANY_PROCESS_KEY)
|
.setBusinessKey(batchCode)
|
.setVariables(variables));
|
for (MesQcSeedQualityDO quality : records) {
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId())
|
.companyProcessInstanceId(processInstanceId).build());
|
}
|
log.info("[maybeStartCompanyProcess][批次 {} 双线质检完成,已发起公司质检审批 {},覆盖 {} 条记录]",
|
batchCode, processInstanceId, records.size());
|
}
|
|
private void startReturnProcess(MesQcSeedQualityDO quality, Long operatorId) {
|
if (quality == null || quality.getReturnSalesId() == null || StrUtil.isNotBlank(quality.getProcessInstanceId()))
|
return;
|
Map<String, Object> variables = new HashMap<>();
|
variables.put("returnSalesId", quality.getReturnSalesId());
|
variables.put("returnSalesCode", quality.getReturnSalesCode());
|
variables.put("code", quality.getCode());
|
String processInstanceId = processInstanceApi.createProcessInstance(operatorId,
|
new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(RETURN_PROCESS_KEY)
|
.setBusinessKey(String.valueOf(quality.getReturnSalesId()))
|
.setVariables(variables));
|
qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId())
|
.processInstanceId(processInstanceId).build());
|
log.info("[startReturnProcess][销售退货单 {} 已发起销售部领导审批 {}]", quality.getReturnSalesId(), processInstanceId);
|
}
|
|
@Override
|
public void retryBpmProcess(Long id) {
|
MesQcSeedQualityDO quality = qualityMapper.selectById(id);
|
if (quality == null) throw invalidParamException("质检记录不存在");
|
Long operatorId = getLoginUserId();
|
if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) {
|
if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS))
|
throw invalidParamException("当前退货质检不需要发起审批");
|
if (StrUtil.isNotBlank(quality.getProcessInstanceId()))
|
throw invalidParamException("退货质检审批流程已经发起");
|
startReturnProcess(quality, operatorId);
|
} else if (Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())) {
|
if (StrUtil.isNotBlank(quality.getProcessInstanceId()))
|
throw invalidParamException("部门审批流程已经发起");
|
startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId);
|
} else if (Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())) {
|
String batchCode = quality.getBatchCode();
|
if (StrUtil.isBlank(batchCode))
|
throw invalidParamException("质检记录未绑定生产批次,不能发起公司审批");
|
List<MesQcSeedQualityDO> batchRecords = qualityMapper.selectListByBatchCode(batchCode).stream()
|
.filter(q -> !isReturnQuality(q))
|
.filter(q -> !Set.of(RELEASED, SCRAPPED).contains(q.getWorkflowStatus()))
|
.toList();
|
if (batchRecords.isEmpty() || batchRecords.stream().anyMatch(q -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL)
|
.contains(q.getWorkflowStatus())))
|
throw invalidParamException("同批次双线部门审批尚未全部完成,不能发起公司审批");
|
if (batchRecords.stream().anyMatch(q -> StrUtil.isNotBlank(q.getCompanyProcessInstanceId())))
|
throw invalidParamException("公司质检审批流程已经发起");
|
maybeStartCompanyProcess(batchCode, operatorId);
|
} else {
|
throw invalidParamException("当前质检状态不需要发起审批");
|
}
|
}
|
|
private void cancelReturnSalesQuietly(Long returnSalesId) {
|
if (returnSalesId == null) return;
|
try {
|
returnSalesService.cancelReturnSales(returnSalesId);
|
} catch (Exception ex) {
|
log.warn("[cancelReturnSalesQuietly][销售退货单 {} 取消失败: {}]", returnSalesId, ex.getMessage());
|
}
|
}
|
|
private void afterCommit(Runnable action) {
|
TransactionSynchronization synchronization = new TransactionSynchronization() {
|
@Override
|
public void afterCommit() {
|
try {
|
action.run();
|
} catch (Exception ex) {
|
log.error("[afterCommit][BPM 流程启动失败,请修复流程配置并在质检记录中重新发起审批]", ex);
|
}
|
}
|
};
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
TransactionSynchronizationManager.registerSynchronization(synchronization);
|
} else {
|
synchronization.afterCommit();
|
}
|
}
|
|
/** 历史数据 bizType 为空,按生产入库质检处理 */
|
private static void requireQcLine(Integer qcLine) {
|
if (!Objects.equals(qcLine, QC_LINE_PROCESS) && !Objects.equals(qcLine, QC_LINE_SALES))
|
throw new IllegalArgumentException("质检线必须为加工部或销售部");
|
}
|
|
/** 生产双线记录只能走 BPM 审批;经销商退货质检不拆分质检线,仍可使用人工审批接口。 */
|
private static void requireLegacyApproval(MesQcSeedQualityDO quality) {
|
if (!isReturnQuality(quality) && (Objects.equals(quality.getQcLine(), QC_LINE_PROCESS)
|
|| Objects.equals(quality.getQcLine(), QC_LINE_SALES)))
|
throw new IllegalArgumentException("该质检记录由 BPM 审批,请在流程待办中处理");
|
}
|
|
private static boolean isReturnQuality(MesQcSeedQualityDO quality) {
|
return Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN);
|
}
|
|
private List<Long> approvalIds(List<Long> ids) {
|
return batchIds(ids, "质检记录");
|
}
|
|
private List<Long> batchIds(List<Long> ids, String label) {
|
if (ids == null || ids.isEmpty() || ids.size() > MAX_BATCH_SIZE || ids.stream().anyMatch(Objects::isNull))
|
throw new IllegalArgumentException("请选择 1 至 " + MAX_BATCH_SIZE + " 条" + label);
|
List<Long> sorted = ids.stream().distinct().sorted().toList();
|
if (sorted.size() != ids.size()) throw new IllegalArgumentException(label + "不能重复选择");
|
return sorted;
|
}
|
|
private void validateLockedIds(List<Long> expectedIds, List<MesQcSeedQualityDO> qualities) {
|
List<Long> actualIds = qualities.stream().map(MesQcSeedQualityDO::getId).sorted().toList();
|
if (!expectedIds.equals(actualIds)) throw new IllegalArgumentException("质检记录不存在或已变化,请刷新后重试");
|
}
|
|
private MesQcSeedQualityDO requireStatus(Long id, int expected) {
|
MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
|
if (quality == null || !Objects.equals(quality.getWorkflowStatus(), expected))
|
throw new IllegalArgumentException("质检记录状态已变化,请刷新后重试");
|
return quality;
|
}
|
|
private record IndicatorMeta(String code, String name, String unit) { }
|
|
private Map<String, IndicatorMeta> requireEffectiveIndicators(Long itemId) {
|
Map<String, IndicatorMeta> result = new LinkedHashMap<>();
|
for (MesQcSeedIndicatorRangeDO range : rangeService.getEffectiveRanges(itemId)) {
|
result.put(range.getIndicatorCode(), new IndicatorMeta(range.getIndicatorCode(),
|
StrUtil.blankToDefault(range.getIndicatorName(), range.getIndicatorCode()),
|
StrUtil.blankToDefault(range.getUnit(), "%")));
|
}
|
if (result.isEmpty())
|
throw new IllegalArgumentException("该产品尚未配置质检项,请先在「质检项合格范围配置」中维护");
|
return result;
|
}
|
|
private Map<String, IndicatorMeta> toMetaMap(Collection<MesQcSeedQualityIndicatorDO> indicators, Long itemId) {
|
Map<String, MesQcSeedIndicatorRangeDO> config = rangeService.getEffectiveMap(itemId);
|
Map<String, IndicatorMeta> result = new LinkedHashMap<>();
|
for (MesQcSeedQualityIndicatorDO indicator : indicators) {
|
MesQcSeedIndicatorRangeDO range = config.get(indicator.getIndicatorCode());
|
String name = StrUtil.blankToDefault(indicator.getIndicatorName(),
|
range == null ? indicator.getIndicatorCode() : range.getIndicatorName());
|
String unit = StrUtil.blankToDefault(indicator.getUnit(), range == null ? "%" : range.getUnit());
|
result.put(indicator.getIndicatorCode(), new IndicatorMeta(indicator.getIndicatorCode(), name, unit));
|
}
|
return result;
|
}
|
|
private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators,
|
Map<String, IndicatorMeta> expected) {
|
if (indicators == null || indicators.isEmpty()) throw new IllegalArgumentException("请至少提交一项质检指标");
|
if (expected.isEmpty()) throw new IllegalArgumentException("该产品尚未配置质检项");
|
if (indicators.size() != expected.size())
|
throw new IllegalArgumentException("质检项数量与产品配置不一致,请刷新后重新录入");
|
Set<String> codes = new HashSet<>();
|
boolean passed = true;
|
for (MesQcSeedQualitySubmitReqVO.Indicator item : indicators) {
|
if (item == null || StrUtil.isBlank(item.getCode()) || !codes.add(item.getCode())
|
|| item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
|
throw new IllegalArgumentException("质检项的实测值和合格范围必须完整且不可重复");
|
if (!expected.containsKey(item.getCode()))
|
throw new IllegalArgumentException("质检项未在产品配置中启用:" + item.getCode());
|
if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("质检项下限不能大于上限");
|
if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getMinValue().compareTo(BigDecimal.ZERO) < 0
|
|| item.getMaxValue().compareTo(BigDecimal.ZERO) < 0)
|
throw new IllegalArgumentException("质检项数值不能为负数");
|
if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
|
}
|
if (!codes.equals(expected.keySet())) throw new IllegalArgumentException("产品质检项不完整,请刷新后重新录入");
|
return passed;
|
}
|
|
private Long resolveItemId(Scope scope) {
|
Long itemId = scope.bags().stream().map(MesProBagCodeDO::getItemId).filter(Objects::nonNull).findFirst().orElse(null);
|
if (itemId != null || scope.bags().isEmpty()) return itemId;
|
Long batchId = scope.bags().get(0).getBatchId();
|
if (batchId == null) return null;
|
MesProBatchDO batch = proBatchService.getBatch(batchId);
|
return batch == null ? null : batch.getItemId();
|
}
|
|
private List<Map<String, Object>> toRangePayload(Long itemId) {
|
List<Map<String, Object>> result = new ArrayList<>();
|
for (MesQcSeedIndicatorRangeDO range : rangeService.getEffectiveRanges(itemId)) {
|
Map<String, Object> item = new HashMap<>();
|
item.put("code", range.getIndicatorCode());
|
item.put("name", StrUtil.blankToDefault(range.getIndicatorName(), range.getIndicatorCode()));
|
item.put("unit", StrUtil.blankToDefault(range.getUnit(), "%"));
|
item.put("minValue", range.getMinValue());
|
item.put("maxValue", range.getMaxValue());
|
result.add(item);
|
}
|
return result;
|
}
|
|
private List<Map<String, Object>> toIndicatorPayload(List<MesQcSeedQualityIndicatorDO> indicators, Long itemId) {
|
Map<String, MesQcSeedIndicatorRangeDO> config = rangeService.getEffectiveMap(itemId);
|
List<Map<String, Object>> result = new ArrayList<>();
|
for (MesQcSeedQualityIndicatorDO indicator : indicators) {
|
MesQcSeedIndicatorRangeDO range = config.get(indicator.getIndicatorCode());
|
Map<String, Object> item = new HashMap<>();
|
item.put("code", indicator.getIndicatorCode());
|
item.put("name", StrUtil.blankToDefault(indicator.getIndicatorName(),
|
range == null ? indicator.getIndicatorCode() : range.getIndicatorName()));
|
item.put("unit", StrUtil.blankToDefault(indicator.getUnit(), range == null ? "%" : range.getUnit()));
|
item.put("minValue", indicator.getMinValue());
|
item.put("maxValue", indicator.getMaxValue());
|
result.add(item);
|
}
|
return result;
|
}
|
|
private void recordAction(Long id, String action, Integer result, String opinion,
|
List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
|
actionMapper.insert(newAction(id, action, result, opinion, indicators, getLoginUserId(), LocalDateTime.now()));
|
}
|
|
private MesQcSeedQualityActionDO newAction(Long id, String action, Integer result, String opinion,
|
List<MesQcSeedQualitySubmitReqVO.Indicator> indicators,
|
Long operatorId, LocalDateTime createTime) {
|
MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO();
|
event.setQualityId(id);
|
event.setAction(action);
|
event.setResult(result);
|
event.setOpinion(opinion);
|
event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators));
|
event.setOperatorId(operatorId);
|
event.setCreateTime(createTime);
|
return event;
|
}
|
|
/**
|
* 定位该袋的暂存库存汇总记录(只定位,不扣减;扣减由质检暂存出库事务完成)。
|
* <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 目标合格库库区编号
|
* @param qualifiedAreaId 目标合格库库位编号
|
*/
|
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;
|
}
|
|
/**
|
* 解析合格库的默认入库库区与库位:取第一个同时配置了库位的非冻结库区。
|
*
|
* 库区/库位为可选项,仓库未配置时按仓库层级直接入库,返回 null。
|
*/
|
private Long[] resolveQualifiedLocation(MesWmWarehouseDO qualifiedWarehouse) {
|
List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId())
|
.stream().filter(location -> !Boolean.TRUE.equals(location.getFrozen())).toList();
|
if (CollUtil.isEmpty(locations)) {
|
return new Long[]{null, null};
|
}
|
for (MesWmWarehouseLocationDO location : locations) {
|
List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(location.getId())
|
.stream().filter(area -> !Boolean.TRUE.equals(area.getFrozen())).toList();
|
if (CollUtil.isNotEmpty(areas)) return new Long[]{location.getId(), areas.get(0).getId()};
|
}
|
return new Long[]{null, null};
|
}
|
|
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();
|
}
|
}
|
|
@Override
|
public List<MesQcSeedQualityActionDO> getActions(Long qualityId) {
|
return actionMapper.selectByQualityId(qualityId);
|
}
|
}
|