package cn.iocoder.yudao.module.srm.service.inquiry; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi; import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseRequestApi; import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO; import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO; import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestRespDTO; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi; import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO; import cn.iocoder.yudao.module.srm.controller.admin.inquiry.vo.*; import cn.iocoder.yudao.module.srm.dal.dataobject.*; import cn.iocoder.yudao.module.srm.dal.mysql.*; import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.srm.enums.InquiryStatusEnum; import cn.iocoder.yudao.module.srm.service.supplier.SrmSupplierService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; /** * SRM 询价比价 Service 实现 */ @Service @Validated @Slf4j public class SrmInquiryServiceImpl implements SrmInquiryService { private static final DateTimeFormatter NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMM"); /** * 采购申请「审核通过」状态值,与 ERP 模块 ErpPurchaseRequestStatusEnum.APPROVE 保持一致 */ private static final Integer PURCHASE_REQUEST_STATUS_APPROVED = 20; @Resource private SrmInquiryMapper inquiryMapper; @Resource private BpmProcessInstanceApi processInstanceApi; @Resource private SrmInquiryItemMapper inquiryItemMapper; @Resource private SrmInquirySupplierMapper inquirySupplierMapper; @Resource private SrmInquiryQuoteMapper inquiryQuoteMapper; @Resource private SrmInquiryOrderMapper inquiryOrderMapper; @Resource private SrmSupplierService supplierService; @Resource private MdmItemApi mdmItemApi; @Resource private MdmUnitMeasureApi mdmUnitMeasureApi; @Resource private ErpPurchaseOrderApi erpPurchaseOrderApi; @Resource private ErpPurchaseRequestApi erpPurchaseRequestApi; @Override @Transactional(rollbackFor = Exception.class) public Long createInquiry(SrmInquirySaveReqVO createReqVO) { validateInquiryInput(createReqVO); SrmInquiryDO inquiry = new SrmInquiryDO(); inquiry.setNo(generateInquiryNo()); inquiry.setTitle(createReqVO.getTitle()); inquiry.setStatus(InquiryStatusEnum.DRAFT.getStatus()); inquiry.setInquiryDate(createReqVO.getInquiryDate() != null ? createReqVO.getInquiryDate() : LocalDate.now()); inquiry.setDeadline(createReqVO.getDeadline()); inquiry.setPurchaserUserId(createReqVO.getPurchaserUserId() != null ? createReqVO.getPurchaserUserId() : getLoginUserId()); inquiry.setRemark(createReqVO.getRemark()); inquiryMapper.insert(inquiry); List items = saveItems(inquiry.getId(), createReqVO.getItems()); saveSuppliers(inquiry.getId(), createReqVO.getSuppliers()); saveItemQuotes(inquiry.getId(), items, createReqVO.getItems(), createReqVO.getSuppliers().stream() .map(SrmInquirySaveReqVO.Supplier::getSupplierId) .collect(Collectors.toSet())); return inquiry.getId(); } @Override @Transactional(rollbackFor = Exception.class) public Long createInquiryFromPurchaseRequest(Long purchaseRequestId) { ErpPurchaseRequestRespDTO purchaseRequest = erpPurchaseRequestApi .getPurchaseRequest(purchaseRequestId).getCheckedData(); if (purchaseRequest == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_EXISTS); } if (!PURCHASE_REQUEST_STATUS_APPROVED.equals(purchaseRequest.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_APPROVED.getCode(), "采购申请【" + purchaseRequest.getNo() + "】当前状态不是「审核通过」,无法发起询价。" + "请先在采购申请中完成审批,审批通过后再发起询价。"); } if (purchaseRequest.getOrderId() != null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_ORDER.getCode(), "采购申请【" + purchaseRequest.getNo() + "】已生成采购台账【" + StrUtil.blankToDefault(purchaseRequest.getOrderNo(), "ID=" + purchaseRequest.getOrderId()) + "】,无法再发起询价。请到采购台账中查看价格明细。"); } SrmInquiryDO linkedInquiry = purchaseRequest.getInquiryId() != null ? inquiryMapper.selectById(purchaseRequest.getInquiryId()) : null; if (linkedInquiry != null && !InquiryStatusEnum.CANCELLED.getStatus().equals(linkedInquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_INQUIRY.getCode(), "采购申请【" + purchaseRequest.getNo() + "】已发起过询价【" + StrUtil.blankToDefault(purchaseRequest.getInquiryNo(), "ID=" + purchaseRequest.getInquiryId()) + "】,请直接打开该询价单继续处理,不要重复发起。"); } List requestItems = purchaseRequest.getItems(); if (CollUtil.isEmpty(requestItems)) { throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(), "采购申请【" + purchaseRequest.getNo() + "】没有明细行,无法发起询价。请回到采购申请补充明细后再试。"); } SrmInquiryDO inquiry = new SrmInquiryDO(); inquiry.setNo(generateInquiryNo()); inquiry.setTitle("采购申请【" + purchaseRequest.getNo() + "】询价"); inquiry.setStatus(InquiryStatusEnum.DRAFT.getStatus()); inquiry.setInquiryDate(LocalDate.now()); inquiry.setPurchaserUserId(getLoginUserId()); inquiry.setRemark("由采购申请【" + purchaseRequest.getNo() + "】发起"); inquiry.setPurchaseRequestId(purchaseRequest.getId()); inquiry.setPurchaseRequestNo(purchaseRequest.getNo()); inquiryMapper.insert(inquiry); List itemVOs = new ArrayList<>(); Set productIds = new HashSet<>(); for (ErpPurchaseRequestRespDTO.Item requestItem : requestItems) { if (!productIds.add(requestItem.getProductId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(), "采购申请【" + purchaseRequest.getNo() + "】中产品【" + resolveItemName(requestItem.getProductId()) + "】存在多行明细,无法直接发起询价。" + "请先在采购申请中合并同一产品的数量后再试。"); } SrmInquirySaveReqVO.Item itemVO = new SrmInquirySaveReqVO.Item(); itemVO.setProductId(requestItem.getProductId()); itemVO.setProductUnitId(requestItem.getProductUnitId()); itemVO.setQuantity(requestItem.getCount()); itemVO.setRemark(requestItem.getRemark()); itemVOs.add(itemVO); } saveItems(inquiry.getId(), itemVOs); erpPurchaseRequestApi.updatePurchaseRequestInquiry(purchaseRequest.getId(), inquiry.getId(), inquiry.getNo()); return inquiry.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateInquiry(SrmInquirySaveReqVO updateReqVO) { SrmInquiryDO inquiry = validateInquiryForUpdate(updateReqVO.getId()); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_EDITABLE.getCode(), "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) + "】,仅草稿状态可编辑。如需调整产品行或供应商,请取消后重新创建询价单。"); } validateInquiryInput(updateReqVO); SrmInquiryDO updateObj = new SrmInquiryDO(); updateObj.setId(inquiry.getId()); updateObj.setTitle(updateReqVO.getTitle()); updateObj.setInquiryDate(updateReqVO.getInquiryDate()); updateObj.setDeadline(updateReqVO.getDeadline()); updateObj.setPurchaserUserId(updateReqVO.getPurchaserUserId()); updateObj.setRemark(updateReqVO.getRemark()); inquiryMapper.updateById(updateObj); // 草稿阶段允许整体替换产品行、供应商与报价 inquiryQuoteMapper.deleteByInquiryId(inquiry.getId()); inquiryItemMapper.deleteByInquiryId(inquiry.getId()); inquirySupplierMapper.deleteByInquiryId(inquiry.getId()); List items = saveItems(inquiry.getId(), updateReqVO.getItems()); saveSuppliers(inquiry.getId(), updateReqVO.getSuppliers()); saveItemQuotes(inquiry.getId(), items, updateReqVO.getItems(), updateReqVO.getSuppliers().stream() .map(SrmInquirySaveReqVO.Supplier::getSupplierId) .collect(Collectors.toSet())); } @Override @Transactional(rollbackFor = Exception.class) public void deleteInquiry(Long id) { SrmInquiryDO inquiry = validateInquiryForUpdate(id); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_EDITABLE.getCode(), "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) + "】,仅草稿状态可删除。已发布或已转订单的询价单请改为取消。"); } inquiryItemMapper.deleteByInquiryId(id); inquirySupplierMapper.deleteByInquiryId(id); inquiryQuoteMapper.deleteByInquiryId(id); inquiryMapper.deleteById(id); clearPurchaseRequestLink(inquiry); } @Override public SrmInquiryDO getInquiry(Long id) { return inquiryMapper.selectById(id); } @Override public SrmInquiryRespVO getInquiryDetail(Long id) { SrmInquiryDO inquiry = validateInquiryExists(id); return buildInquiryResp(inquiry); } @Override public PageResult getInquiryPage(SrmInquiryPageReqVO pageReqVO) { return inquiryMapper.selectPage(pageReqVO); } @Override public List getInquirySimpleList() { return inquiryMapper.selectList(null).stream() .filter(item -> !InquiryStatusEnum.CANCELLED.getStatus().equals(item.getStatus())) .sorted(Comparator.comparing(SrmInquiryDO::getId).reversed()) .collect(Collectors.toList()); } @Override @Transactional(rollbackFor = Exception.class) public void submitApproval(Long id, Long userId) { SrmInquiryDO inquiry = validateInquiryForUpdate(id); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "当前状态不可提交审核"); } List items = inquiryItemMapper.selectListByInquiryId(id); if (CollUtil.isEmpty(items)) throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_ITEM_EMPTY); Map quotes = inquiryQuoteMapper.selectListByInquiryId(id).stream() .collect(Collectors.toMap(SrmInquiryQuoteDO::getId, Function.identity())); Set suppliers = inquirySupplierMapper.selectListByInquiryId(id).stream() .map(SrmInquirySupplierDO::getSupplierId).collect(Collectors.toSet()); for (SrmInquiryItemDO item : items) { SrmInquiryQuoteDO quote = quotes.get(item.getSelectedQuoteId()); if (quote == null || !item.getId().equals(quote.getInquiryItemId()) || !suppliers.contains(quote.getSupplierId()) || quote.getPrice() == null || quote.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_REQUIRED.getCode(), "请先为每个产品选择有效的中标报价"); } inquiryItemMapper.updateById(new SrmInquiryItemDO().setId(item.getId()) .setSelectedSupplierId(quote.getSupplierId()).setSelectedPrice(quote.getPrice())); } inquiryMapper.updateById(new SrmInquiryDO().setId(id).setStatus(InquiryStatusEnum.APPROVING.getStatus()).setProcessInstanceId("")); String processId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey("srm_inquiry_approve").setBusinessKey(String.valueOf(id)) .setVariables(Map.of("inquiryNo", inquiry.getNo(), "inquiryTitle", inquiry.getTitle()))); inquiryMapper.updateById(new SrmInquiryDO().setId(id).setProcessInstanceId(processId)); } @Override @Transactional(rollbackFor = Exception.class) public void updateApprovalStatus(Long id, String processInstanceId, Integer status) { SrmInquiryDO inquiry = validateInquiryForUpdate(id); if (!InquiryStatusEnum.APPROVING.getStatus().equals(inquiry.getStatus()) || (StrUtil.isNotEmpty(inquiry.getProcessInstanceId()) && !Objects.equals(inquiry.getProcessInstanceId(), processInstanceId))) return; Integer next = null; if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(status)) next = InquiryStatusEnum.APPROVED.getStatus(); if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(status) || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(status)) next = InquiryStatusEnum.REJECTED.getStatus(); if (next != null) inquiryMapper.updateById(new SrmInquiryDO().setId(id).setStatus(next)); } @Override @Transactional(rollbackFor = Exception.class) public void publishInquiry(Long id) { SrmInquiryDO inquiry = validateInquiryForUpdate(id); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_QUOTING.getCode(), "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) + "】,仅草稿状态可发布。"); } inquiryMapper.updateByIdAndStatus(id, InquiryStatusEnum.DRAFT.getStatus(), new SrmInquiryDO().setStatus(InquiryStatusEnum.QUOTING.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void cancelInquiry(Long id) { SrmInquiryDO inquiry = validateInquiryForUpdate(id); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CANCELLABLE.getCode(), "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) + "】,仅草稿或询价中状态可取消。"); } inquiryMapper.updateById(new SrmInquiryDO() .setId(id).setStatus(InquiryStatusEnum.CANCELLED.getStatus())); // 询价取消后解除采购申请关联,采购员可重新选择「发起询价」或「直接生成台账」 clearPurchaseRequestLink(inquiry); } @Override @Transactional(rollbackFor = Exception.class) public void saveQuotes(SrmInquiryQuoteSaveReqVO saveReqVO) { SrmInquiryDO inquiry = validateInquiryForUpdate(saveReqVO.getInquiryId()); if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_QUOTING.getCode(), "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) + "】,仅草稿或询价中状态可录入报价。"); } List items = inquiryItemMapper.selectListByInquiryId(inquiry.getId()); Map itemMap = items.stream() .collect(Collectors.toMap(SrmInquiryItemDO::getId, Function.identity())); List participants = inquirySupplierMapper.selectListByInquiryId(inquiry.getId()); Set participantIds = participants.stream() .map(SrmInquirySupplierDO::getSupplierId).collect(Collectors.toSet()); Set touchedSupplierIds = new HashSet<>(); for (SrmInquiryQuoteSaveReqVO.Quote quoteVO : saveReqVO.getQuotes()) { SrmInquiryItemDO item = itemMap.get(quoteVO.getInquiryItemId()); if (item == null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_QUOTE_NOT_EXISTS.getCode(), "询价产品行ID=" + quoteVO.getInquiryItemId() + " 不属于询价单【" + inquiry.getNo() + "】,请刷新比价页面后重新录入报价。"); } if (!participantIds.contains(quoteVO.getSupplierId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_SUPPLIER_NOT_PARTICIPANT.getCode(), "供应商ID=" + quoteVO.getSupplierId() + " 未参与询价单【" + inquiry.getNo() + "】,请先在询价单中添加该供应商。"); } SrmInquiryQuoteDO existing = inquiryQuoteMapper.selectByItemIdAndSupplierId( quoteVO.getInquiryItemId(), quoteVO.getSupplierId()); if (quoteVO.getPrice() == null) { // 清空报价 if (existing != null) { inquiryQuoteMapper.deleteById(existing.getId()); touchedSupplierIds.add(quoteVO.getSupplierId()); } continue; } if (quoteVO.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException(ErrorCodeConstants.INQUIRY_QUOTE_PRICE_INVALID.getCode(), "产品【" + item.getProductName() + "】供应商ID=" + quoteVO.getSupplierId() + " 的报价单价为【" + quoteVO.getPrice() + "】,必须大于 0,请修改后重新保存。"); } SrmInquiryQuoteDO quote = BeanUtils.toBean(quoteVO, SrmInquiryQuoteDO.class); quote.setInquiryId(inquiry.getId()); quote.setIsSelected(existing != null && Boolean.TRUE.equals(existing.getIsSelected())); if (existing != null) { quote.setId(existing.getId()); inquiryQuoteMapper.updateById(quote); } else { inquiryQuoteMapper.insert(quote); } touchedSupplierIds.add(quoteVO.getSupplierId()); } // 刷新参与供应商报价状态 LocalDateTime now = LocalDateTime.now(); for (Long supplierId : touchedSupplierIds) { SrmInquirySupplierDO participant = inquirySupplierMapper .selectByInquiryIdAndSupplierId(inquiry.getId(), supplierId); if (participant == null) { continue; } Long quoteCount = inquiryQuoteMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(SrmInquiryQuoteDO::getInquiryId, inquiry.getId()) .eq(SrmInquiryQuoteDO::getSupplierId, supplierId)); SrmInquirySupplierDO updateObj = new SrmInquirySupplierDO(); updateObj.setId(participant.getId()); updateObj.setQuoteStatus(quoteCount != null && quoteCount > 0 ? 10 : 0); updateObj.setQuoteTime(now); inquirySupplierMapper.updateById(updateObj); } } @Override public SrmInquiryRespVO getCompare(Long id) { return getInquiryDetail(id); } @Override @Transactional(rollbackFor = Exception.class) public SrmInquiryConfirmRespVO confirmInquiry(SrmInquiryConfirmReqVO confirmReqVO) { SrmInquiryDO inquiry = validateInquiryForUpdate(confirmReqVO.getId()); if (!InquiryStatusEnum.APPROVED.getStatus().equals(inquiry.getStatus())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "询价单审核通过后才能转订单"); } if (CollUtil.isNotEmpty(confirmReqVO.getSelections())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "审核通过后不能更改中标结果"); } List items = inquiryItemMapper.selectListByInquiryId(inquiry.getId()); if (CollUtil.isEmpty(items)) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_ITEM_EMPTY); } Map itemMap = items.stream() .collect(Collectors.toMap(SrmInquiryItemDO::getId, Function.identity())); Set participantIds = inquirySupplierMapper.selectListByInquiryId(inquiry.getId()).stream() .map(SrmInquirySupplierDO::getSupplierId).collect(Collectors.toSet()); List allQuotes = inquiryQuoteMapper.selectListByInquiryId(inquiry.getId()); Map quoteMap = allQuotes.stream() .collect(Collectors.toMap(SrmInquiryQuoteDO::getId, Function.identity())); // 1. 校验每个产品恰好选择一条有效报价 // 未显式传 selections 时,使用询价单中已保存的中标选择(创建/编辑时一并录入) List selections = confirmReqVO.getSelections(); if (CollUtil.isEmpty(selections)) { selections = new ArrayList<>(); for (SrmInquiryItemDO item : items) { if (item.getSelectedQuoteId() != null) { SrmInquiryConfirmReqVO.Selection selection = new SrmInquiryConfirmReqVO.Selection(); selection.setInquiryItemId(item.getId()); selection.setQuoteId(item.getSelectedQuoteId()); selections.add(selection); } } if (CollUtil.isEmpty(selections)) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_REQUIRED.getCode(), "询价单【" + inquiry.getNo() + "】尚未保存任何中标选择,请编辑询价单为每个产品选择中标供应商后再确认。"); } } Map selectedByItem = new HashMap<>(); for (SrmInquiryConfirmReqVO.Selection selection : selections) { SrmInquiryItemDO item = itemMap.get(selection.getInquiryItemId()); if (item == null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_QUOTE_MISMATCH.getCode(), "询价产品行ID=" + selection.getInquiryItemId() + " 不属于询价单【" + inquiry.getNo() + "】,请刷新比价页面后重新选择中标报价。"); } if (selectedByItem.containsKey(item.getId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_DUPLICATE.getCode(), "产品【" + item.getProductName() + "】选择了多条中标报价,请为每个产品只保留一条中标报价。"); } SrmInquiryQuoteDO quote = quoteMap.get(selection.getQuoteId()); if (quote == null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_QUOTE_NOT_EXISTS.getCode(), "中标报价ID=" + selection.getQuoteId() + " 不存在,请刷新比价页面后重新选择。"); } if (!quote.getInquiryItemId().equals(item.getId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_QUOTE_MISMATCH.getCode(), "产品【" + item.getProductName() + "】选择的中标报价属于产品行ID=" + quote.getInquiryItemId() + ",请重新选择该产品自己的报价。"); } if (!participantIds.contains(quote.getSupplierId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_SUPPLIER_NOT_PARTICIPANT.getCode(), "供应商ID=" + quote.getSupplierId() + " 未参与询价单【" + inquiry.getNo() + "】,无法作为中标供应商。"); } if (quote.getPrice() == null || quote.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException(ErrorCodeConstants.INQUIRY_QUOTE_PRICE_INVALID.getCode(), "产品【" + item.getProductName() + "】的中标报价单价为空或小于等于 0,请先录入报价后再确认。"); } selectedByItem.put(item.getId(), quote); } for (SrmInquiryItemDO item : items) { if (!selectedByItem.containsKey(item.getId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_REQUIRED.getCode(), "产品【" + StrUtil.blankToDefault(item.getProductName(), "ID=" + item.getProductId()) + "】尚未选择中标供应商,请为该产品选择一条中标报价后再确认。"); } } // 2. 记录中标结果 for (SrmInquiryQuoteDO quote : allQuotes) { if (Boolean.TRUE.equals(quote.getIsSelected())) { inquiryQuoteMapper.updateById(new SrmInquiryQuoteDO().setId(quote.getId()).setIsSelected(false)); } } for (Map.Entry entry : selectedByItem.entrySet()) { SrmInquiryItemDO item = itemMap.get(entry.getKey()); SrmInquiryQuoteDO quote = entry.getValue(); inquiryQuoteMapper.updateById(new SrmInquiryQuoteDO().setId(quote.getId()).setIsSelected(true)); inquiryItemMapper.updateById(new SrmInquiryItemDO() .setId(item.getId()) .setSelectedSupplierId(quote.getSupplierId()) .setSelectedQuoteId(quote.getId()) .setSelectedPrice(quote.getPrice())); } // 3. 按中标供应商分组生成采购订单草稿 Map> winnersBySupplier = new LinkedHashMap<>(); for (Map.Entry entry : selectedByItem.entrySet()) { QuoteWithItem pair = new QuoteWithItem(itemMap.get(entry.getKey()), entry.getValue()); winnersBySupplier.computeIfAbsent(entry.getValue().getSupplierId(), k -> new ArrayList<>()).add(pair); } Map supplierMap = supplierService.getSupplierMap(winnersBySupplier.keySet()); List orders = new ArrayList<>(); for (Map.Entry> entry : winnersBySupplier.entrySet()) { Long supplierId = entry.getKey(); SrmSupplierDO supplier = supplierMap.get(supplierId); String supplierName = supplier != null ? supplier.getName() : "ID=" + supplierId; List orderItems = new ArrayList<>(); for (QuoteWithItem pair : entry.getValue()) { SrmInquiryItemDO item = pair.item; SrmInquiryQuoteDO quote = pair.quote; Long productUnitId = item.getProductUnitId(); if (productUnitId == null) { MdmItemRespDTO mdmItem = mdmItemApi.getItem(item.getProductId()).getCheckedData(); productUnitId = mdmItem != null ? mdmItem.getUnitMeasureId() : null; } ErpPurchaseOrderCreateReqDTO.Item orderItem = new ErpPurchaseOrderCreateReqDTO.Item(); orderItem.setProductId(item.getProductId()); orderItem.setProductUnitId(productUnitId); orderItem.setProductPrice(quote.getPrice()); orderItem.setCount(item.getQuantity()); orderItem.setTaxPercent(quote.getTaxRate()); orderItem.setRemark("SRM询价 " + inquiry.getNo()); // 与采购申请/采购台账一致:新生成的采购明细默认质检 orderItem.setQcCheckFlag(true); orderItems.add(orderItem); } ErpPurchaseOrderCreateReqDTO createReqDTO = new ErpPurchaseOrderCreateReqDTO(); createReqDTO.setSupplierId(supplierId); createReqDTO.setOrderTime(LocalDateTime.now()); createReqDTO.setPaymentDate(confirmReqVO.getPaymentDate().atStartOfDay()); createReqDTO.setDiscountPercent(BigDecimal.ZERO); createReqDTO.setDepositPrice(BigDecimal.ZERO); createReqDTO.setSourceType(2); createReqDTO.setSourceNo(inquiry.getNo()); createReqDTO.setSourceId(inquiry.getId()); createReqDTO.setPurchaseRequestId(inquiry.getPurchaseRequestId()); createReqDTO.setRemark("SRM询价自动生成,询价单号:" + inquiry.getNo()); createReqDTO.setItems(orderItems); Long purchaseOrderId; try { purchaseOrderId = erpPurchaseOrderApi.createPurchaseOrderDraft(createReqDTO).getCheckedData(); } catch (Exception ex) { log.error("[confirmInquiry] 询价单({}) 供应商({}) 生成采购订单失败", inquiry.getNo(), supplierId, ex); throw new ServiceException(ErrorCodeConstants.INQUIRY_ORDER_GENERATE_FAIL.getCode(), "询价单【" + inquiry.getNo() + "】为供应商【" + supplierName + "】生成采购订单失败:" + ex.getMessage() + "。请检查该供应商与产品资料后重试。"); } String purchaseOrderNo = null; try { ErpPurchaseOrderRespDTO order = erpPurchaseOrderApi.getPurchaseOrder(purchaseOrderId).getCheckedData(); purchaseOrderNo = order != null ? order.getNo() : null; } catch (Exception ex) { log.warn("[confirmInquiry] 获取采购订单号失败,purchaseOrderId={}", purchaseOrderId, ex); } inquiryOrderMapper.insert(SrmInquiryOrderDO.builder() .inquiryId(inquiry.getId()) .supplierId(supplierId) .purchaseOrderId(purchaseOrderId) .purchaseOrderNo(purchaseOrderNo) .build()); SrmInquiryConfirmRespVO.Order orderResult = new SrmInquiryConfirmRespVO.Order(); orderResult.setSupplierId(supplierId); orderResult.setSupplierName(supplierName); orderResult.setPurchaseOrderId(purchaseOrderId); orderResult.setPurchaseOrderNo(purchaseOrderNo); orders.add(orderResult); } // 3.1 询价单来源于采购申请时,回写采购申请关联的采购台账 // 一个询价单可能按中标供应商拆成多张台账,采购申请只记录第一张,完整清单可通过台账的采购申请字段反查 if (inquiry.getPurchaseRequestId() != null && CollUtil.isNotEmpty(orders)) { SrmInquiryConfirmRespVO.Order firstOrder = orders.get(0); if (orders.size() > 1) { log.info("[confirmInquiry] 询价单({}) 为采购申请({}) 生成 {} 张台账,采购申请仅回写首张台账({})", inquiry.getNo(), inquiry.getPurchaseRequestNo(), orders.size(), firstOrder.getPurchaseOrderNo()); } try { erpPurchaseRequestApi.updatePurchaseRequestOrder(inquiry.getPurchaseRequestId(), firstOrder.getPurchaseOrderId(), firstOrder.getPurchaseOrderNo()); } catch (Exception ex) { log.error("[confirmInquiry] 询价单({}) 回写采购申请({}) 关联台账失败", inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex); throw new ServiceException(ErrorCodeConstants.INQUIRY_ORDER_GENERATE_FAIL.getCode(), "询价单【" + inquiry.getNo() + "】已生成采购台账【" + StrUtil.blankToDefault(firstOrder.getPurchaseOrderNo(), "ID=" + firstOrder.getPurchaseOrderId()) + "】,但回写采购申请【" + inquiry.getPurchaseRequestNo() + "】失败:" + ex.getMessage() + "。请到采购申请中核对关联台账,或联系管理员处理。"); } } // 4. 更新询价单为已转订单(状态条件更新,避免并发重复生成) int updated = inquiryMapper.updateByIdAndStatus(inquiry.getId(), inquiry.getStatus(), new SrmInquiryDO().setStatus(InquiryStatusEnum.CONFIRMED.getStatus())); if (updated == 0) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_ALREADY_CONFIRMED); } SrmInquiryConfirmRespVO result = new SrmInquiryConfirmRespVO(); result.setInquiryId(inquiry.getId()); result.setOrders(orders); return result; } // ========== 组装 ========== private SrmInquiryRespVO buildInquiryResp(SrmInquiryDO inquiry) { SrmInquiryRespVO respVO = BeanUtils.toBean(inquiry, SrmInquiryRespVO.class); List items = inquiryItemMapper.selectListByInquiryId(inquiry.getId()); List participants = inquirySupplierMapper.selectListByInquiryId(inquiry.getId()); List quotes = inquiryQuoteMapper.selectListByInquiryId(inquiry.getId()); List inquiryOrders = inquiryOrderMapper.selectListByInquiryId(inquiry.getId()); Set supplierIds = new HashSet<>(); participants.forEach(p -> supplierIds.add(p.getSupplierId())); quotes.forEach(q -> supplierIds.add(q.getSupplierId())); inquiryOrders.forEach(o -> supplierIds.add(o.getSupplierId())); Map supplierMap = supplierService.getSupplierMap(supplierIds); respVO.setItems(BeanUtils.toBean(items, SrmInquiryRespVO.Item.class)); respVO.setQuotes(BeanUtils.toBean(quotes, SrmInquiryRespVO.Quote.class)); List supplierVOs = BeanUtils.toBean(participants, SrmInquiryRespVO.Supplier.class); supplierVOs.forEach(vo -> { SrmSupplierDO supplier = supplierMap.get(vo.getSupplierId()); if (supplier != null) { vo.setSupplierName(supplier.getName()); vo.setSupplierCode(supplier.getCode()); } }); respVO.setSuppliers(supplierVOs); List orderVOs = BeanUtils.toBean(inquiryOrders, SrmInquiryRespVO.Order.class); orderVOs.forEach(vo -> { SrmSupplierDO supplier = supplierMap.get(vo.getSupplierId()); if (supplier != null) { vo.setSupplierName(supplier.getName()); } }); respVO.setOrders(orderVOs); return respVO; } // ========== 保存辅助 ========== private void validateInquiryInput(SrmInquirySaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getItems())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_ITEM_EMPTY); } if (CollUtil.isEmpty(reqVO.getSuppliers())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_SUPPLIER_EMPTY); } Set productIds = new HashSet<>(); for (SrmInquirySaveReqVO.Item item : reqVO.getItems()) { if (!productIds.add(item.getProductId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_ITEM_EMPTY.getCode(), "产品ID=" + item.getProductId() + " 在询价单中重复添加,请为每个产品只保留一行。"); } if (item.getQuantity() == null || item.getQuantity().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException(ErrorCodeConstants.INQUIRY_ITEM_EMPTY.getCode(), "产品【" + StrUtil.blankToDefault(item.getProductName(), "ID=" + item.getProductId()) + "】的采购数量必须大于 0,请填写数量后重新保存。"); } } Set supplierIds = new HashSet<>(); for (SrmInquirySaveReqVO.Supplier supplier : reqVO.getSuppliers()) { if (!supplierIds.add(supplier.getSupplierId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_SUPPLIER_DUPLICATE.getCode(), "供应商ID=" + supplier.getSupplierId() + " 在询价单中重复添加,请为每个供应商只保留一行。"); } } validateSuppliers(supplierIds); } private void validateSuppliers(Collection supplierIds) { List suppliers = supplierService.getSupplierList(supplierIds); Map map = suppliers.stream() .collect(Collectors.toMap(SrmSupplierDO::getId, Function.identity())); for (Long supplierId : supplierIds) { SrmSupplierDO supplier = map.get(supplierId); if (supplier == null) { throw new ServiceException(ErrorCodeConstants.SUPPLIER_NOT_EXISTS.getCode(), "供应商ID=" + supplierId + " 不存在,请刷新供应商列表后重新选择。"); } if (supplier.getStatus() != null && !CommonStatusEnum.ENABLE.getStatus().equals(supplier.getStatus())) { throw new ServiceException(ErrorCodeConstants.SUPPLIER_NOT_ENABLE.getCode(), "供应商【" + supplier.getName() + "】已停用,请先在供应商档案中启用后再参与询价。"); } } } private List saveItems(Long inquiryId, List itemVOs) { List savedItems = new ArrayList<>(); Set productIds = itemVOs.stream().map(SrmInquirySaveReqVO.Item::getProductId) .collect(Collectors.toSet()); Map itemMap = mdmItemApi.getItemList(productIds).getCheckedData().stream() .collect(Collectors.toMap(MdmItemRespDTO::getId, Function.identity())); Set unitIds = itemMap.values().stream().map(MdmItemRespDTO::getUnitMeasureId) .filter(Objects::nonNull).collect(Collectors.toSet()); Map unitMap = unitIds.isEmpty() ? Collections.emptyMap() : mdmUnitMeasureApi.getUnitMeasureMap(unitIds); for (int i = 0; i < itemVOs.size(); i++) { SrmInquirySaveReqVO.Item itemVO = itemVOs.get(i); MdmItemRespDTO mdmItem = itemMap.get(itemVO.getProductId()); if (mdmItem == null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_ITEM_EMPTY.getCode(), "产品ID=" + itemVO.getProductId() + " 不存在,请重新选择产品。"); } SrmInquiryItemDO item = BeanUtils.toBean(itemVO, SrmInquiryItemDO.class); item.setId(null); item.setInquiryId(inquiryId); item.setProductCode(StrUtil.blankToDefault(itemVO.getProductCode(), mdmItem.getCode())); item.setProductName(StrUtil.blankToDefault(itemVO.getProductName(), mdmItem.getName())); item.setProductSpec(StrUtil.blankToDefault(itemVO.getProductSpec(), mdmItem.getSpecification())); Long productUnitId = itemVO.getProductUnitId() != null ? itemVO.getProductUnitId() : mdmItem.getUnitMeasureId(); item.setProductUnitId(productUnitId); if (StrUtil.isBlank(itemVO.getUnitName()) && productUnitId != null) { MdmUnitMeasureRespDTO unit = unitMap.get(productUnitId); item.setUnitName(unit != null ? unit.getName() : null); } item.setSort(i); inquiryItemMapper.insert(item); savedItems.add(item); } return savedItems; } /** * 保存产品行报价与中标选择(创建/编辑询价单时一并写入) */ private void saveItemQuotes(Long inquiryId, List items, List itemVOs, Set participantIds) { for (int i = 0; i < items.size(); i++) { SrmInquiryItemDO item = items.get(i); SrmInquirySaveReqVO.Item itemVO = itemVOs.get(i); String productLabel = StrUtil.blankToDefault(item.getProductName(), "ID=" + item.getProductId()); List quoteVOs = itemVO.getQuotes() == null ? Collections.emptyList() : itemVO.getQuotes(); Set quoteSupplierIds = new HashSet<>(); SrmInquiryQuoteDO selectedQuote = null; for (SrmInquirySaveReqVO.Quote quoteVO : quoteVOs) { if (!quoteSupplierIds.add(quoteVO.getSupplierId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_SUPPLIER_DUPLICATE.getCode(), "产品【" + productLabel + "】的供应商ID=" + quoteVO.getSupplierId() + " 报价重复,请为每个供应商只保留一条报价。"); } if (!participantIds.contains(quoteVO.getSupplierId())) { throw new ServiceException(ErrorCodeConstants.INQUIRY_SUPPLIER_NOT_PARTICIPANT.getCode(), "供应商ID=" + quoteVO.getSupplierId() + " 未参与询价单,无法录入产品【" + productLabel + "】的报价,请先在参与供应商中添加该供应商。"); } if (quoteVO.getPrice() == null || quoteVO.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException(ErrorCodeConstants.INQUIRY_QUOTE_PRICE_INVALID.getCode(), "产品【" + productLabel + "】供应商ID=" + quoteVO.getSupplierId() + " 的报价单价为【" + quoteVO.getPrice() + "】,必须大于 0,请修改后重新保存。"); } boolean selected = itemVO.getSelectedSupplierId() != null && itemVO.getSelectedSupplierId().equals(quoteVO.getSupplierId()); SrmInquiryQuoteDO quote = BeanUtils.toBean(quoteVO, SrmInquiryQuoteDO.class); quote.setInquiryId(inquiryId); quote.setInquiryItemId(item.getId()); quote.setIsSelected(selected); inquiryQuoteMapper.insert(quote); if (selected) { selectedQuote = quote; } } Long selectedSupplierId = itemVO.getSelectedSupplierId(); if (selectedSupplierId != null && selectedQuote == null) { throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_QUOTE_MISMATCH.getCode(), "产品【" + productLabel + "】选择的中标供应商ID=" + selectedSupplierId + " 没有录入报价,请先填写该供应商的报价,或改选其他已报价供应商。"); } if (selectedQuote != null) { inquiryItemMapper.updateById(new SrmInquiryItemDO() .setId(item.getId()) .setSelectedSupplierId(selectedQuote.getSupplierId()) .setSelectedQuoteId(selectedQuote.getId()) .setSelectedPrice(selectedQuote.getPrice())); } } } private void saveSuppliers(Long inquiryId, List supplierVOs) { for (SrmInquirySaveReqVO.Supplier supplierVO : supplierVOs) { inquirySupplierMapper.insert(SrmInquirySupplierDO.builder() .inquiryId(inquiryId) .supplierId(supplierVO.getSupplierId()) .quoteStatus(0) .remark(supplierVO.getRemark()) .build()); } } // ========== 校验 ========== private SrmInquiryDO validateInquiryForUpdate(Long id) { SrmInquiryDO inquiry = inquiryMapper.selectByIdForUpdate(id); if (inquiry == null) throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_NOT_EXISTS); return inquiry; } private SrmInquiryDO validateInquiryExists(Long id) { SrmInquiryDO inquiry = inquiryMapper.selectById(id); if (inquiry == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_NOT_EXISTS); } return inquiry; } private String generateInquiryNo() { String date = LocalDate.now().format(NO_DATE_FORMAT); for (int attempt = 0; attempt < 20; attempt++) { int seq = (int) (Math.random() * 9000) + 1000; String no = "XJ" + date + String.format("%04d", seq); if (inquiryMapper.selectByNo(no) == null) { return no; } } throw new ServiceException(ErrorCodeConstants.INQUIRY_NO_GENERATE_FAIL.getCode(), "询价单号在多次尝试后仍重复,请稍后重试。"); } /** * 询价单被取消/删除时,解除对采购申请的关联,让采购员可重新选择「发起询价」或「直接生成台账」。 * 解除失败只记录日志,不阻断取消操作;发起询价时会再按询价单状态做兜底判断。 */ private void clearPurchaseRequestLink(SrmInquiryDO inquiry) { if (inquiry.getPurchaseRequestId() == null) { return; } try { erpPurchaseRequestApi.clearPurchaseRequestInquiry(inquiry.getPurchaseRequestId()); } catch (Exception ex) { log.warn("[clearPurchaseRequestLink] 询价单({}) 解除采购申请({}) 关联失败,请人工核对", inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex); } } /** * 解析产品名称,仅用于异常提示;解析失败时回退为可读 ID,避免二次异常 */ private String resolveItemName(Long productId) { if (productId == null) { return "未选择"; } try { MdmItemRespDTO item = mdmItemApi.getItem(productId).getCheckedData(); if (item != null && StrUtil.isNotBlank(item.getName())) { return item.getName(); } } catch (Exception ex) { log.warn("[resolveItemName] 查询产品({}) 名称失败", productId, ex); } return "ID=" + productId; } private String statusName(Integer status) { for (InquiryStatusEnum e : InquiryStatusEnum.values()) { if (e.getStatus().equals(status)) { return e.getName(); } } return "未知"; } private static class QuoteWithItem { private final SrmInquiryItemDO item; private final SrmInquiryQuoteDO quote; private QuoteWithItem(SrmInquiryItemDO item, SrmInquiryQuoteDO quote) { this.item = item; this.quote = quote; } } }