package cn.iocoder.yudao.module.srm.service.tender; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi; import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO; import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO; import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO; import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectSaveReqVO; import cn.iocoder.yudao.module.srm.dal.dataobject.*; import cn.iocoder.yudao.module.srm.dal.mysql.*; import cn.iocoder.yudao.module.srm.enums.AwardStatusEnum; import cn.iocoder.yudao.module.srm.enums.BiddingModeEnum; import cn.iocoder.yudao.module.srm.enums.BidStatusEnum; import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.starter.annotation.LogRecord; 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.math.RoundingMode; import java.time.LocalDateTime; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*; @Service @Validated @Slf4j public class SrmTenderProjectServiceImpl implements SrmTenderProjectService { @Resource private SrmTenderProjectMapper tenderProjectMapper; @Resource private SrmTenderMaterialMapper tenderMaterialMapper; @Resource private SrmBidAwardMapper bidAwardMapper; @Resource private SrmTenderBidMapper tenderBidMapper; @Resource private SrmTenderConfigService tenderConfigService; @Resource private SrmSupplierMapper supplierMapper; @Resource private SrmSupplierQuoteMapper supplierQuoteMapper; @Resource private MdmItemApi mdmItemApi; @Resource private ErpPurchaseOrderApi erpPurchaseOrderApi; @Override public Long createTenderProject(SrmTenderProjectSaveReqVO createReqVO) { SrmTenderProjectDO project = BeanUtils.toBean(createReqVO, SrmTenderProjectDO.class); project.setTenderStatus(TenderStatusEnum.DRAFT.getCode()); if (project.getBiddingMode() == null || project.getBiddingMode().isEmpty()) { project.setBiddingMode(tenderConfigService.getConfig().getDefaultBiddingMode()); } tenderProjectMapper.insert(project); return project.getId(); } @Override @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS) public void updateTenderProject(SrmTenderProjectSaveReqVO updateReqVO) { SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId()); Integer status = project.getTenderStatus(); // 草稿状态:全字段可编辑 if (TenderStatusEnum.canEditFullProject(status)) { SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class); updateObj.setTenderStatus(status); // 状态不允许前端传入覆盖 tenderProjectMapper.updateById(updateObj); LogRecordContext.putVariable("tenderName", project.getTenderName()); LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); return; } // 发布中/投标中:仅允许修改招标说明和资质要求 if (TenderStatusEnum.canEditPartialProject(status)) { SrmTenderProjectDO updateObj = new SrmTenderProjectDO(); updateObj.setId(updateReqVO.getId()); updateObj.setTenderDesc(updateReqVO.getTenderDesc()); updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements()); tenderProjectMapper.updateById(updateObj); LogRecordContext.putVariable("tenderName", project.getTenderName()); LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); return; } // 其他状态:不允许编辑 throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); } @Override public void deleteTenderProject(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); } // 检查是否已有投标数据 if (tenderBidMapper.countByTenderProjectId(id) > 0) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_HAS_BIDS_CANNOT_DELETE); } tenderMaterialMapper.deleteByTenderProjectId(id); tenderProjectMapper.deleteById(id); } @Override public SrmTenderProjectDO getTenderProject(Long id) { SrmTenderProjectDO project = tenderProjectMapper.selectById(id); if (project != null) { SrmBidAwardDO award = bidAwardMapper.selectByTenderProjectId(id); if (award != null) { project.setAwardId(award.getId()); project.setPurchaseOrderNo(award.getPurchaseOrderNo()); } } return project; } @Override public PageResult getTenderProjectPage(SrmTenderProjectPageReqVO pageReqVO) { PageResult pageResult = tenderProjectMapper.selectPage(pageReqVO); // 批量查询定标信息,填充 awardId 和 purchaseOrderNo List projectIds = pageResult.getList().stream() .map(SrmTenderProjectDO::getId).collect(Collectors.toList()); if (!projectIds.isEmpty()) { List awards = bidAwardMapper.selectListByTenderProjectIds(projectIds); Map awardMap = awards.stream().collect(Collectors.toMap( SrmBidAwardDO::getTenderProjectId, Function.identity(), (a, b) -> a)); pageResult.getList().forEach(project -> { SrmBidAwardDO award = awardMap.get(project.getId()); if (award != null) { project.setAwardId(award.getId()); project.setPurchaseOrderNo(award.getPurchaseOrderNo()); // 防御:如果定标已生成订单但项目状态未更新,同步状态 if (AwardStatusEnum.ORDER_GENERATED.getCode().equals(award.getAwardStatus())) { project.setTenderStatus(TenderStatusEnum.CLOSED.getCode()); } } }); } return pageResult; } @Override public void publishTender(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); } project.setTenderStatus(TenderStatusEnum.PUBLISHED.getCode()); tenderProjectMapper.updateById(project); } @Override public void closeTender(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (TenderStatusEnum.CLOSED.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_ALREADY_CLOSED); } // 草稿状态不允许直接关闭 if (TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); } project.setTenderStatus(TenderStatusEnum.CLOSED.getCode()); tenderProjectMapper.updateById(project); } @Override public void startBidding(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (!TenderStatusEnum.PUBLISHED.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_PUBLISHED); } project.setTenderStatus(TenderStatusEnum.BIDDING.getCode()); tenderProjectMapper.updateById(project); } @Override public void startEvaluation(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (!TenderStatusEnum.BIDDING.getCode().equals(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_BIDDING); } project.setTenderStatus(TenderStatusEnum.EVALUATING.getCode()); tenderProjectMapper.updateById(project); } // ========== 简易模式操作 ========== @Override public void startPriceComparing(Long id) { SrmTenderProjectDO project = validateTenderExists(id); if (!BiddingModeEnum.isSimple(project.getBiddingMode())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MODE_NOT_SIMPLE); } if (!TenderStatusEnum.canStartPriceComparing(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); } // 检查是否有物料 if (tenderMaterialMapper.countByTenderProjectId(id) == 0) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NO_MATERIALS); } project.setTenderStatus(TenderStatusEnum.PRICE_COMPARING.getCode()); tenderProjectMapper.updateById(project); } @Override @Transactional(rollbackFor = Exception.class) public void confirmTenderProject(Long id, Long bidId) { // 1. 校验项目状态 SrmTenderProjectDO project = validateTenderExists(id); if (!BiddingModeEnum.isSimple(project.getBiddingMode())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MODE_NOT_SIMPLE); } if (!TenderStatusEnum.canConfirm(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_PRICE_COMPARING); } // 2. 校验投标记录 SrmTenderBidDO bid = tenderBidMapper.selectById(bidId); if (bid == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_NOT_EXISTS); } if (!bid.getTenderProjectId().equals(id)) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_BID_NOT_IN_PROJECT); } if (BidStatusEnum.WITHDRAWN.getCode().equals(bid.getBidStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_BID_WITHDRAWN); } // 3. 创建定标记录(直接已定标,跳过审批) SrmBidAwardDO award = new SrmBidAwardDO(); award.setTenderProjectId(id); award.setSupplierId(bid.getSupplierId()); award.setBidId(bidId); award.setAwardNo("SMP" + System.currentTimeMillis()); award.setAwardStatus(AwardStatusEnum.AWARDED.getCode()); award.setAwardAmount(bid.getBidTotalAmount()); award.setAwardTime(LocalDateTime.now()); award.setApprovalTime(LocalDateTime.now()); bidAwardMapper.insert(award); // 4. 设置投标状态为中标 bid.setBidStatus(BidStatusEnum.WIN.getCode()); tenderBidMapper.updateById(bid); // 5. 自动生成采购订单 generatePurchaseOrderForSimple(award, project); // 6. 更新项目状态 project.setTenderStatus(TenderStatusEnum.CONFIRMED.getCode()); tenderProjectMapper.updateById(project); } /** * 简易模式:根据定标信息自动生成采购订单 */ private void generatePurchaseOrderForSimple(SrmBidAwardDO award, SrmTenderProjectDO project) { // 查供应商 SrmSupplierDO supplier = supplierMapper.selectById(award.getSupplierId()); if (supplier == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED); } // 查招标物料 List materials = tenderMaterialMapper .selectListByTenderProjectId(award.getTenderProjectId()); if (CollUtil.isEmpty(materials)) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS); } // 查中标报价 List quotes = supplierQuoteMapper.selectListByBidId(award.getBidId()); if (CollUtil.isEmpty(quotes)) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_QUOTE_NOT_EXISTS); } Map quoteMap = quotes.stream() .collect(Collectors.toMap( SrmSupplierQuoteDO::getTenderMaterialId, Function.identity(), (a, b) -> a)); // 查 MDM 物料 Set productIds = materials.stream() .map(SrmTenderMaterialDO::getProductId) .collect(Collectors.toSet()); Map itemMap = mdmItemApi.getItemList(productIds).getCheckedData() .stream().collect(Collectors.toMap(MdmItemRespDTO::getId, Function.identity())); // 计算价格 BigDecimal totalQuoteAmount = quotes.stream() .map(SrmSupplierQuoteDO::getQuotePrice) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal awardAmount = award.getAwardAmount() != null ? award.getAwardAmount() : totalQuoteAmount; BigDecimal ratio = totalQuoteAmount.compareTo(BigDecimal.ZERO) > 0 ? awardAmount.divide(totalQuoteAmount, 8, RoundingMode.HALF_UP) : BigDecimal.ONE; // 构建订单项 List orderItems = new ArrayList<>(); for (SrmTenderMaterialDO material : materials) { SrmSupplierQuoteDO quote = quoteMap.get(material.getId()); if (quote == null) { continue; } MdmItemRespDTO mdmItem = itemMap.get(material.getProductId()); if (mdmItem == null) { continue; } BigDecimal materialTotal = quote.getQuotePrice().multiply(ratio); BigDecimal unitPrice = materialTotal.divide(material.getQuantity(), 4, RoundingMode.HALF_UP); ErpPurchaseOrderCreateReqDTO.Item item = new ErpPurchaseOrderCreateReqDTO.Item(); item.setProductId(material.getProductId()); item.setProductUnitId(mdmItem.getUnitMeasureId()); item.setProductPrice(unitPrice); item.setCount(material.getQuantity()); item.setTaxPercent(quote.getTaxRate()); item.setRemark("定标编号: " + award.getAwardNo()); orderItems.add(item); } // 调用 ERP 创建采购订单 ErpPurchaseOrderCreateReqDTO createReq = new ErpPurchaseOrderCreateReqDTO(); createReq.setSupplierId(supplier.getId()); createReq.setOrderTime(LocalDateTime.now()); createReq.setDepositPrice(BigDecimal.ZERO); createReq.setItems(orderItems); createReq.setRemark("SRM 简易招投标自动生成, 定标编号: " + award.getAwardNo()); Long purchaseOrderId = erpPurchaseOrderApi.createPurchaseOrder(createReq).getCheckedData(); // 获取采购订单号 String purchaseOrderNo = null; try { purchaseOrderNo = erpPurchaseOrderApi.getPurchaseOrder(purchaseOrderId) .getCheckedData().getNo(); } catch (Exception e) { log.warn("[generatePurchaseOrderForSimple] 获取采购订单号失败,purchaseOrderId={}", purchaseOrderId, e); } // 更新定标记录 award.setPurchaseOrderId(purchaseOrderId); award.setPurchaseOrderNo(purchaseOrderNo); award.setAwardStatus(AwardStatusEnum.ORDER_GENERATED.getCode()); bidAwardMapper.updateById(award); } @Override @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS) public void updateTenderProjectPartial(SrmTenderProjectPartialUpdateReqVO updateReqVO) { SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId()); if (!TenderStatusEnum.canEditPartialProject(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); } SrmTenderProjectDO updateObj = new SrmTenderProjectDO(); updateObj.setId(updateReqVO.getId()); updateObj.setTenderDesc(updateReqVO.getTenderDesc()); updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements()); tenderProjectMapper.updateById(updateObj); LogRecordContext.putVariable("tenderName", project.getTenderName()); LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); } // ========== 物料子表 ========== @Override public void createMaterial(SrmTenderMaterialSaveReqVO createReqVO) { SrmTenderProjectDO project = validateTenderExists(createReqVO.getTenderProjectId()); if (!TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); } SrmTenderMaterialDO material = BeanUtils.toBean(createReqVO, SrmTenderMaterialDO.class); tenderMaterialMapper.insert(material); } @Override public void updateMaterial(SrmTenderMaterialSaveReqVO updateReqVO) { SrmTenderMaterialDO existing = tenderMaterialMapper.selectById(updateReqVO.getId()); if (existing != null) { SrmTenderProjectDO project = tenderProjectMapper.selectById(existing.getTenderProjectId()); if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); } } SrmTenderMaterialDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderMaterialDO.class); tenderMaterialMapper.updateById(updateObj); } @Override public void deleteMaterial(Long id) { SrmTenderMaterialDO material = tenderMaterialMapper.selectById(id); if (material != null) { SrmTenderProjectDO project = tenderProjectMapper.selectById(material.getTenderProjectId()); if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); } } tenderMaterialMapper.deleteById(id); } @Override public List getMaterialList(Long tenderProjectId) { return tenderMaterialMapper.selectListByTenderProjectId(tenderProjectId); } private SrmTenderProjectDO validateTenderExists(Long id) { SrmTenderProjectDO project = tenderProjectMapper.selectById(id); if (project == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS); } return project; } }