refactor(storage): 优化附件关联逻辑并增加评标功能
- 统一使用 updateAttachments 方法替代手动删除和绑定附件的逻辑
- 为评标控制器添加供应商名称查询和展示功能
- 在报价响应中增加物料基础信息字段
- 优化投标报价创建时的数据验证和自动补全逻辑
- 增加根据招标项目获取定标的查询接口
- 修复附件查询时缺少应用标识的问题
- 调整文件工具类中的附件删除逻辑,只删除关联记录不删除文件本身
| | |
| | | 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 |
| | |
| | | // 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) { |
| | |
| | | // 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) { |
| | |
| | | // 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) { |
| | |
| | | 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) { |
| | |
| | | // 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) { |
| | |
| | | // 更新 |
| | | 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 |
| | |
| | | } |
| | | 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 |
| | |
| | | } |
| | | 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 |
| | |
| | | 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 |
| | |
| | | // 更新 |
| | | 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 |
| | |
| | | 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) |
| | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | |
| | | @Resource |
| | | private SrmBidEvaluationService bidEvaluationService; |
| | | |
| | | @Resource |
| | | private SrmSupplierService supplierService; |
| | | |
| | | @PostMapping("/create") |
| | | @Operation(summary = "创建评标记录") |
| | | @PreAuthorize("@ss.hasPermission('srm:bid-evaluation:create')") |
| | |
| | | @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") |
| | |
| | | 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; |
| | |
| | | |
| | | @Resource |
| | | private SrmSupplierService supplierService; |
| | | |
| | | @Resource |
| | | private SrmTenderMaterialMapper tenderMaterialMapper; |
| | | |
| | | // ========== 投标管理 ========== |
| | | |
| | |
| | | @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()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // ========== 关联数据填充 ========== |
| | |
| | | @Schema(description = "供应商ID", example = "1") |
| | | private Long supplierId; |
| | | |
| | | @Schema(description = "供应商名称", example = "华为技术") |
| | | private String supplierName; |
| | | |
| | | @Schema(description = "投标ID", example = "1") |
| | | private Long bidId; |
| | | |
| | |
| | | @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; |
| | | |
| | |
| | | SrmBidAwardDO getAward(Long id); |
| | | |
| | | /** |
| | | * 根据招标项目ID获取定标记录 |
| | | */ |
| | | SrmBidAwardDO getAwardByProjectId(Long tenderProjectId); |
| | | |
| | | /** |
| | | * 审批定标 |
| | | */ |
| | | void approveAward(Long id); |
| | |
| | | } |
| | | |
| | | @Override |
| | | public SrmBidAwardDO getAwardByProjectId(Long tenderProjectId) { |
| | | return bidAwardMapper.selectByTenderProjectId(tenderProjectId); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void approveAward(Long id) { |
| | | SrmBidAwardDO award = validateAwardExists(id); |
| | |
| | | @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(); |
| | | } |
| | |
| | | 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) { |
| | |
| | | |
| | | 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); |
| | |
| | | } |
| | | |
| | | @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); |
| | |
| | | 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; |
| | | } |
| | | |