11 小时以前 2b8f88856e90e530ddbfce88aea107a0ec894d5d
refactor(storage): 优化附件关联逻辑并增加评标功能

- 统一使用 updateAttachments 方法替代手动删除和绑定附件的逻辑
- 为评标控制器添加供应商名称查询和展示功能
- 在报价响应中增加物料基础信息字段
- 优化投标报价创建时的数据验证和自动补全逻辑
- 增加根据招标项目获取定标的查询接口
- 修复附件查询时缺少应用标识的问题
- 调整文件工具类中的附件删除逻辑,只删除关联记录不删除文件本身
已修改22个文件
191 ■■■■■ 文件已修改
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidAwardController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidEvaluationController.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderBidController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmBidEvaluationRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmSupplierQuoteRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApi.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApiImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/util/storage/StorageFileUtil.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -128,11 +128,7 @@
        saleQuotationMapper.updateById(updateObj);
        // 2.2 更新报价明细
        updateQuotationProductList(updateReqVO.getId(), quotationProducts);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("crm_sale_quotation", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "crm_sale_quotation",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "crm_sale_quotation", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -125,11 +125,7 @@
        // 2.2 更新付款单项
        updateFinancePaymentItemList(updateReqVO.getId(), paymentItems);
        // 2.3 更新附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("erp_finance_payment", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "erp_finance_payment",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "erp_finance_payment", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    private void calculateTotalPrice(ErpFinancePaymentDO payment, List<ErpFinancePaymentItemDO> paymentItems) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java
@@ -125,11 +125,7 @@
        // 2.2 更新收款单项
        updateFinanceReceiptItemList(updateReqVO.getId(), receiptItems);
        // 2.3 更新附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("erp_finance_receipt", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "erp_finance_receipt",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "erp_finance_receipt", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    private void calculateTotalPrice(ErpFinanceReceiptDO receipt, List<ErpFinanceReceiptItemDO> receiptItems) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -147,11 +147,7 @@
        // 2.2 更新订单项
        updatePurchaseOrderItemList(updateReqVO.getId(), purchaseOrderItems);
        // 2.3 更新附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("erp_purchase_order", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "erp_purchase_order",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "erp_purchase_order", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -146,11 +146,7 @@
        updatePurchaseRequestItemList(updateReqVO.getId(), requestItems);
        // 6. 更新附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("erp_purchase_request", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "erp_purchase_request",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "erp_purchase_request", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -141,11 +141,7 @@
        // 2.2 更新订单项
        updateSaleOrderItemList(updateReqVO.getId(), saleOrderItems);
        // 2.3 更新附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("erp_sale_order", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "erp_sale_order",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "erp_sale_order", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    private void calculateTotalPrice(ErpSaleOrderDO saleOrder, List<ErpSaleOrderItemDO> saleOrderItems) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
@@ -121,12 +121,7 @@
        // 更新
        HrmLeaveApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmLeaveApplicationDO.class);
        leaveApplicationMapper.updateById(updateObj);
        // 重新绑定附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_leave_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_leave_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "hrm_leave_application", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -127,11 +127,7 @@
        }
        HrmResignationApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmResignationApplicationDO.class);
        resignationApplicationMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_resignation_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_resignation_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "hrm_resignation_application", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -134,11 +134,7 @@
        }
        HrmTransferApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmTransferApplicationDO.class);
        transferApplicationMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_transfer_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_transfer_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "hrm_transfer_application", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java
@@ -64,11 +64,7 @@
        validateAttendanceExceptionExists(updateReqVO.getId());
        HrmAttendanceExceptionDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceExceptionDO.class);
        attendanceExceptionMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_attendance_exception", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_attendance_exception",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "hrm_attendance_exception", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java
@@ -55,11 +55,7 @@
        // 更新
        MesPdDocumentDO updateObj = BeanUtils.toBean(updateReqVO, MesPdDocumentDO.class);
        documentMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("mes_pd_document", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "mes_pd_document",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
        storageAttachmentApi.updateAttachments("file", "mes_pd_document", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidAwardController.java
@@ -38,6 +38,13 @@
        return success(BeanUtils.toBean(bidAwardService.getAward(id), SrmBidAwardRespVO.class));
    }
    @GetMapping("/get-by-project")
    @Operation(summary = "根据招标项目ID获取定标详情")
    @PreAuthorize("@ss.hasPermission('srm:bid-award:query')")
    public CommonResult<SrmBidAwardRespVO> getAwardByProject(@RequestParam("tenderProjectId") Long tenderProjectId) {
        return success(BeanUtils.toBean(bidAwardService.getAwardByProjectId(tenderProjectId), SrmBidAwardRespVO.class));
    }
    @PutMapping("/approve")
    @Operation(summary = "审批定标")
    @Parameter(name = "id", description = "定标ID", required = true)
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidEvaluationController.java
@@ -1,8 +1,11 @@
package cn.iocoder.yudao.module.srm.controller.admin.tender;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.*;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.service.supplier.SrmSupplierService;
import cn.iocoder.yudao.module.srm.service.tender.SrmBidEvaluationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -13,6 +16,8 @@
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -25,6 +30,9 @@
    @Resource
    private SrmBidEvaluationService bidEvaluationService;
    @Resource
    private SrmSupplierService supplierService;
    @PostMapping("/create")
    @Operation(summary = "创建评标记录")
    @PreAuthorize("@ss.hasPermission('srm:bid-evaluation:create')")
@@ -36,7 +44,21 @@
    @Operation(summary = "按招标项目获取评标列表")
    @PreAuthorize("@ss.hasPermission('srm:bid-evaluation:query')")
    public CommonResult<List<SrmBidEvaluationRespVO>> getEvaluationList(@RequestParam("tenderProjectId") Long tenderProjectId) {
        return success(BeanUtils.toBean(bidEvaluationService.getEvaluationList(tenderProjectId), SrmBidEvaluationRespVO.class));
        List<SrmBidEvaluationRespVO> list = BeanUtils.toBean(bidEvaluationService.getEvaluationList(tenderProjectId), SrmBidEvaluationRespVO.class);
        enrichEvaluationSupplierNames(list);
        return success(list);
    }
    private void enrichEvaluationSupplierNames(List<SrmBidEvaluationRespVO> list) {
        if (list == null || list.isEmpty()) return;
        Set<Long> supplierIds = CollectionUtils.convertSet(list, SrmBidEvaluationRespVO::getSupplierId);
        Map<Long, SrmSupplierDO> supplierMap = supplierService.getSupplierMap(supplierIds);
        list.forEach(vo -> {
            SrmSupplierDO supplier = supplierMap.get(vo.getSupplierId());
            if (supplier != null) {
                vo.setSupplierName(supplier.getName());
            }
        });
    }
    @PutMapping("/calculate-ranking")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderBidController.java
@@ -5,6 +5,8 @@
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.*;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderMaterialMapper;
import cn.iocoder.yudao.module.srm.service.supplier.SrmSupplierService;
import cn.iocoder.yudao.module.srm.service.tender.SrmTenderBidService;
import io.swagger.v3.oas.annotations.Operation;
@@ -33,6 +35,9 @@
    @Resource
    private SrmSupplierService supplierService;
    @Resource
    private SrmTenderMaterialMapper tenderMaterialMapper;
    // ========== 投标管理 ==========
@@ -91,7 +96,25 @@
    @Operation(summary = "按投标获取报价列表")
    @PreAuthorize("@ss.hasPermission('srm:supplier-quote:query')")
    public CommonResult<List<SrmSupplierQuoteRespVO>> getQuoteListByBid(@RequestParam("bidId") Long bidId) {
        return success(BeanUtils.toBean(tenderBidService.getQuoteList(bidId), SrmSupplierQuoteRespVO.class));
        List<SrmSupplierQuoteRespVO> list = BeanUtils.toBean(tenderBidService.getQuoteList(bidId), SrmSupplierQuoteRespVO.class);
        enrichQuoteMaterialInfo(list);
        return success(list);
    }
    private void enrichQuoteMaterialInfo(List<SrmSupplierQuoteRespVO> list) {
        if (list == null || list.isEmpty()) return;
        Set<Long> materialIds = CollectionUtils.convertSet(list, SrmSupplierQuoteRespVO::getTenderMaterialId);
        Map<Long, SrmTenderMaterialDO> materialMap = tenderMaterialMapper.selectBatchIds(materialIds).stream()
                .collect(java.util.stream.Collectors.toMap(SrmTenderMaterialDO::getId, m -> m));
        list.forEach(vo -> {
            SrmTenderMaterialDO material = materialMap.get(vo.getTenderMaterialId());
            if (material != null) {
                vo.setTenderMaterialCode(material.getProductCode());
                vo.setTenderMaterialName(material.getProductName());
                vo.setTenderMaterialSpec(material.getProductSpec());
                vo.setTenderMaterialUnit(material.getUnit());
            }
        });
    }
    // ========== 关联数据填充 ==========
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmBidEvaluationRespVO.java
@@ -19,6 +19,9 @@
    @Schema(description = "供应商ID", example = "1")
    private Long supplierId;
    @Schema(description = "供应商名称", example = "华为技术")
    private String supplierName;
    @Schema(description = "投标ID", example = "1")
    private Long bidId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmSupplierQuoteRespVO.java
@@ -25,6 +25,18 @@
    @Schema(description = "招标物料ID", example = "1")
    private Long tenderMaterialId;
    @Schema(description = "物料编码")
    private String tenderMaterialCode;
    @Schema(description = "物料名称")
    private String tenderMaterialName;
    @Schema(description = "物料规格")
    private String tenderMaterialSpec;
    @Schema(description = "物料单位")
    private String tenderMaterialUnit;
    @Schema(description = "报价价格", example = "5000")
    private BigDecimal quotePrice;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardService.java
@@ -20,6 +20,11 @@
    SrmBidAwardDO getAward(Long id);
    /**
     * 根据招标项目ID获取定标记录
     */
    SrmBidAwardDO getAwardByProjectId(Long tenderProjectId);
    /**
     * 审批定标
     */
    void approveAward(Long id);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java
@@ -84,6 +84,11 @@
    }
    @Override
    public SrmBidAwardDO getAwardByProjectId(Long tenderProjectId) {
        return bidAwardMapper.selectByTenderProjectId(tenderProjectId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approveAward(Long id) {
        SrmBidAwardDO award = validateAwardExists(id);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java
@@ -88,12 +88,15 @@
    @Override
    public Long createQuote(SrmSupplierQuoteSaveReqVO createReqVO) {
        // 校验投标状态(撤标后不允许报价)
        validateBidNotWithdrawn(createReqVO.getBidId());
        SrmTenderBidDO bid = validateBidNotWithdrawn(createReqVO.getBidId());
        // 校验同一投标下物料不可重复报价
        if (supplierQuoteMapper.existsByBidAndMaterial(createReqVO.getBidId(), createReqVO.getTenderMaterialId())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTE_MATERIAL_DUPLICATE);
        }
        SrmSupplierQuoteDO quote = BeanUtils.toBean(createReqVO, SrmSupplierQuoteDO.class);
        // 从投标记录中获取招标项目ID和供应商ID,避免前端遗漏导致数据库约束失败
        quote.setTenderProjectId(bid.getTenderProjectId());
        quote.setSupplierId(bid.getSupplierId());
        supplierQuoteMapper.insert(quote);
        return quote.getId();
    }
@@ -128,11 +131,12 @@
        return bid;
    }
    private void validateBidNotWithdrawn(Long bidId) {
    private SrmTenderBidDO validateBidNotWithdrawn(Long bidId) {
        SrmTenderBidDO bid = validateBidExists(bidId);
        if (BidStatusEnum.WITHDRAWN.getCode().equals(bid.getBidStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_WITHDRAWN_CANNOT_QUOTE);
        }
        return bid;
    }
    private void validateTenderStatus(Long tenderProjectId) {
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApi.java
@@ -8,6 +8,18 @@
    void bindAttachments(String application, String recordType, Long recordId, List<Long> blobIds);
    /**
     * 增量更新附件关联:
     * - 查询现有附件,对比新旧 blobId,只删除多余、只绑定新增
     * - 如果新旧一致,不做任何操作
     *
     * @param application 应用用途
     * @param recordType  业务记录类型
     * @param recordId    业务记录 ID
     * @param newBlobIds  新的 blobId 列表(为 null 时跳过,不做任何操作)
     */
    void updateAttachments(String application, String recordType, Long recordId, List<Long> newBlobIds);
    List<StorageBlobRespDTO> listAttachments(String recordType, Long recordId);
    void deleteAttachments(List<Long> attachmentIds);
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApiImpl.java
@@ -38,8 +38,24 @@
    }
    @Override
    public void updateAttachments(String application, String recordType, Long recordId, List<Long> newBlobIds) {
        if (newBlobIds == null) {
            return;
        }
        if (newBlobIds.isEmpty()) {
            // 清空所有附件
            systemStorageAttachmentService.deleteAttachmentsByApplicationAndRecordTypeAndRecordId(
                    application != null ? application : "file", recordType, recordId);
            return;
        }
        // 全量替换:bindAttachments 内部先删后插,直接传入全部新 blobId 即可
        bindAttachments(application, recordType, recordId, newBlobIds);
    }
    @Override
    public List<StorageBlobRespDTO> listAttachments(String recordType, Long recordId) {
        SystemStorageAttachmentListReqVO reqVO = new SystemStorageAttachmentListReqVO();
        reqVO.setApplication("file");
        reqVO.setRecordType(recordType);
        reqVO.setRecordId(recordId);
        List<SystemStorageBlobRespVO> list = systemStorageAttachmentService.listAttachments(reqVO);
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/util/storage/StorageFileUtil.java
@@ -104,21 +104,20 @@
            throw new IllegalArgumentException("关联记录id不能为空");
        }
        // 删除旧附件信息
        // 只删除旧的附件关联记录,保留 blob 记录(文件本身不删除)
        if (application == null || application.trim().isEmpty()) {
            for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) {
                String itemApp = item.getApplication();
                if (itemApp == null || itemApp.trim().isEmpty()) {
                    throw new IllegalArgumentException("文件用途不能为空");
                }
                deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(itemApp, recordType.getType(), recordId);
                storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordId(itemApp, recordType.getType(), recordId);
            }
        } else {
            deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType.getType(), recordId);
            storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordId(application, recordType.getType(), recordId);
        }
        if (CollUtil.isEmpty(blobItems)) {
            deleteStorageAttachmentsByRecordTypeAndRecordId(recordType.getType(), recordId);
            return;
        }