liu
7 天以前 ee982b85c9ff09fbf6a6eb7c133b1e6f5fb160ba
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java
@@ -1,32 +1,52 @@
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.system.api.storage.StorageAttachmentApi;
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.SrmBidAwardDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderProjectDO;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidAwardMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderMaterialMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderProjectMapper;
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.module.srm.service.apply.SrmSupplierApplyService;
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.util.List;
import java.util.Map;
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 {
    /**
     * 招标项目资质要求附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#SRM_TENDER_PROJECT}
     */
    public static final String TENDER_PROJECT_RECORD_TYPE = "srm_tender_project";
    @Resource
    private SrmTenderProjectMapper tenderProjectMapper;
@@ -34,31 +54,97 @@
    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;
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    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);
        // 绑定资质要求附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", TENDER_PROJECT_RECORD_TYPE, project.getId(), createReqVO.getBlobIds());
        }
        return project.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @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());
        if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT);
        Integer status = project.getTenderStatus();
        // 草稿状态:全字段可编辑
        if (TenderStatusEnum.canEditFullProject(status)) {
            SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
            updateObj.setTenderStatus(status); // 状态不允许前端传入覆盖
            tenderProjectMapper.updateById(updateObj);
            // 增量更新资质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            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;
        }
        SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
        tenderProjectMapper.updateById(updateObj);
        // 发布中/投标中:仅允许修改招标说明和资质要求
        if (TenderStatusEnum.canEditPartialProject(status)) {
            SrmTenderProjectDO updateObj = new SrmTenderProjectDO();
            updateObj.setId(updateReqVO.getId());
            updateObj.setTenderDesc(updateReqVO.getTenderDesc());
            updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
            tenderProjectMapper.updateById(updateObj);
            // 增量更新资质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            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
    @Transactional(rollbackFor = Exception.class)
    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);
        }
        // 删除资质要求附件
        storageAttachmentApi.deleteAttachmentsByRecord(TENDER_PROJECT_RECORD_TYPE, id);
        tenderMaterialMapper.deleteByTenderProjectId(id);
        tenderProjectMapper.deleteById(id);
    }
@@ -117,6 +203,10 @@
        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);
    }
@@ -141,22 +231,226 @@
        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);
        }
        // 供应商未准入则自动准入并通过,保证采购订单流程可用
        supplierApplyService.ensureSupplierApproved(supplier.getId());
        // 查招标物料
        List<SrmTenderMaterialDO> materials = tenderMaterialMapper
                .selectListByTenderProjectId(award.getTenderProjectId());
        if (CollUtil.isEmpty(materials)) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS);
        }
        // 查中标报价
        List<SrmSupplierQuoteDO> quotes = supplierQuoteMapper.selectListByBidId(award.getBidId());
        if (CollUtil.isEmpty(quotes)) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_QUOTE_NOT_EXISTS);
        }
        Map<Long, SrmSupplierQuoteDO> quoteMap = quotes.stream()
                .collect(Collectors.toMap(
                        SrmSupplierQuoteDO::getTenderMaterialId,
                        Function.identity(),
                        (a, b) -> a));
        // 查 MDM 物料
        Set<Long> productIds = materials.stream()
                .map(SrmTenderMaterialDO::getProductId)
                .collect(Collectors.toSet());
        Map<Long, MdmItemRespDTO> 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<ErpPurchaseOrderCreateReqDTO.Item> 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
    @Transactional(rollbackFor = Exception.class)
    @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);
        // 增量更新资质要求附件
        storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
        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);
    }