liyong
2026-05-08 09d3d31899bb346c6fe8ad5d89168730b5dfae4a
refactor(business): 重构业务机会和销售台账的文件管理功能

- 将 BusinessDescriptionDto 中的 storageBlobDTOS 字段重命名为 businessCommonFiles
- 移除不必要的 Wrappers 导入并优化查询逻辑
- 重构业务机会服务中的文件查询逻辑,支持业务描述和商机主表文件的统一获取
- 在文件工具类中为存储blob对象添加 URL 和名称字段
- 更新销售台账服务接口和实现,添加删除台账文件功能
- 修改销售台账控制器返回类型为统一的 R 对象
- 重构销售台账新增/更新逻辑,集成文件附件绑定功能
- 移除临时文件迁移相关代码,简化文件处理流程
已修改9个文件
267 ■■■■■ 文件已修改
src/main/java/com/ruoyi/basic/dto/BusinessDescriptionDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/StorageBlobVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/BusinessOpportunityServiceImpl.java 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/utils/FileUtil.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 174 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/BusinessDescriptionDto.java
@@ -9,7 +9,7 @@
public class BusinessDescriptionDto extends BusinessDescription {
    private List<StorageBlobDTO> storageBlobDTOS;
    private List<StorageBlobDTO> businessCommonFiles;
    private List<StorageBlobVO> storageBlobVO;
}
src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java
@@ -15,6 +15,9 @@
     */
    private String downloadURL;
    private String url;
    private String name;
    /**
     * 文件类型
     */
src/main/java/com/ruoyi/basic/dto/StorageBlobVO.java
@@ -15,5 +15,8 @@
     */
    private String downloadURL;
    private String url;
    private String name;
    private Long storageAttachmentId;
}
src/main/java/com/ruoyi/basic/service/impl/BusinessOpportunityServiceImpl.java
@@ -2,7 +2,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.BusinessDescriptionDto;
@@ -56,11 +55,41 @@
        LoginUser loginUser = SecurityUtils.getLoginUser();
        IPage<BusinessOpportunityDto> businessOpportunityDtoIPage = businessOpportunityMapper.listPage(page, businessOpportunityDto);
        businessOpportunityDtoIPage.getRecords().forEach(item -> {
            item.setBusinessCommonFiles(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.BUSINESS_OPPORTUNITY, item.getId()));
            item.setBusinessDescription(businessDescriptionMapper.selectList(Wrappers.lambdaQuery(BusinessDescription.class)
                    .eq(BusinessDescription::getBusinessOpportunityId, item.getId())
                    .orderByDesc(BusinessDescription::getCreateTime)));
        businessOpportunityDtoIPage.getRecords().forEach(opportunity -> {
            ArrayList<StorageBlobVO> storageBlobVOS = new ArrayList<>();
            // 查询业务描述列表
            List<BusinessDescription> businessDescriptions = businessDescriptionMapper.selectList(
                    new LambdaQueryWrapper<BusinessDescription>()
                            .eq(BusinessDescription::getBusinessOpportunityId, opportunity.getId())
                            .orderByDesc(BusinessDescription::getCreateTime)
            );
            // 收集每个业务描述的文件
            if (businessDescriptions != null && !businessDescriptions.isEmpty()) {
                businessDescriptions.forEach(description -> {
                    List<StorageBlobVO> files = fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(
                            ApplicationTypeEnum.FILE,
                            RecordTypeEnum.BUSINESS_DESCRIPTION,
                            Long.valueOf(description.getId())
                    );
                    if (files != null && !files.isEmpty()) {
                        storageBlobVOS.addAll(files);
                    }
                });
            }
            // 查询商机主表的文件
            List<StorageBlobVO> opportunityFiles = fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(
                    ApplicationTypeEnum.FILE,
                    RecordTypeEnum.BUSINESS_OPPORTUNITY,
                    opportunity.getId()
            );
            if (opportunityFiles != null && !opportunityFiles.isEmpty()) {
                storageBlobVOS.addAll(opportunityFiles);
            }
            opportunity.setBusinessCommonFiles(storageBlobVOS);
            opportunity.setBusinessDescription(businessDescriptions);
        });
        return businessOpportunityDtoIPage;
@@ -130,7 +159,8 @@
            unipushService.sendClientMessage(sysNoticeList);
        }
        int insert = businessDescriptionMapper.insert(businessDescription);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.BUSINESS_DESCRIPTION, businessDescription.getBusinessOpportunityId(),  businessDescription.getStorageBlobDTOS());
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.BUSINESS_DESCRIPTION, Long.valueOf(businessDescription.getId()),  businessDescription.getBusinessCommonFiles());
        return insert > 0 ? R.ok() : R.fail();
    }
src/main/java/com/ruoyi/basic/utils/FileUtil.java
@@ -1,9 +1,7 @@
package com.ruoyi.basic.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageAttachmentVO;
import com.ruoyi.basic.dto.StorageBlobDTO;
@@ -344,6 +342,8 @@
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
@@ -387,16 +387,18 @@
        List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
        List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
        List<StorageBlobVO> storageBlobDTOS = new ArrayList<>();
        List<StorageBlobVO> storageBlobVOS = new ArrayList<>();
        for (StorageBlob storageBlob : storageBlobs) {
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
            storageBlobVOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
        return storageBlobVOS;
    }
    /**
@@ -417,6 +419,8 @@
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobDTOS.add(storageBlobVO);
        }
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -192,11 +192,11 @@
     */
    @Log(title = "销售台账", businessType = BusinessType.DELETE)
    @DeleteMapping("/delLedger")
    public AjaxResult remove(@RequestBody Long[] ids) {
    public R remove(@RequestBody Long[] ids) {
        if (ids == null || ids.length == 0) {
            return AjaxResult.error("请传入要删除的ID");
            return R.fail("请传入要删除的ID");
        }
        return toAjax(salesLedgerService.deleteSalesLedgerByIds(ids));
        return R.ok(salesLedgerService.deleteSalesLedgerByIds(ids));
    }
    /**
@@ -216,11 +216,11 @@
     */
    @Log(title = "销售台账附件删除", businessType = BusinessType.DELETE)
    @DeleteMapping("/delLedgerFile")
    public AjaxResult delLedgerFile(@RequestBody Long[] ids) {
    public R delLedgerFile(@RequestBody Long[] ids) {
        if (ids == null || ids.length == 0) {
            return AjaxResult.error("请传入要删除的ID");
            return R.fail("请传入要删除的ID");
        }
        return toAjax(commonFileService.deleteSalesLedgerByIds(ids));
        return R.ok(salesLedgerService.delLdgerFile(ids));
    }
    /**
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -1,9 +1,5 @@
package com.ruoyi.sales.pojo;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
@@ -11,6 +7,10 @@
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
 * 销售台账对象 sales_ledger
 *
src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
@@ -3,7 +3,6 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.aftersalesservice.pojo.AfterSalesService;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.sales.dto.LossProductModelDto;
@@ -12,10 +11,10 @@
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.vo.SalesLedgerVo;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.util.List;
@@ -55,4 +54,6 @@
    List<LossProductModelDto> getSalesLedgerWithProductsLoss(Long salesLedgerId);
    IPage<SalesLedgerDto> listSalesLedger(SalesLedgerDto salesLedgerDto, Page page);
    Boolean delLdgerFile(Long[] ids);
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -9,10 +9,13 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.common.exception.base.BaseException;
@@ -24,8 +27,9 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.mapper.ProductionProductInputMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
@@ -41,7 +45,6 @@
import com.ruoyi.sales.vo.SalesLedgerVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -52,15 +55,10 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
@@ -118,6 +116,8 @@
    ;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private FileUtil fileUtil;
    @Override
    public List<SalesLedger> selectSalesLedgerList(SalesLedgerDto salesLedgerDto) {
@@ -487,6 +487,12 @@
        return salesLedgerDtoIPage;
    }
    @Override
    public Boolean delLdgerFile(Long[] ids) {
        fileUtil.deleteStorageBlobsByStorageAttachmentIds(List.of(ids));
        return true;
    }
    /**
     * 下划线命名转驼峰命名
     */
@@ -582,125 +588,47 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
        try {
            // 1. 校验客户信息
            Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
            if (customer == null) {
                throw new BaseException("客户不存在");
            }
            // 2. DTO转Entity
            SalesLedger salesLedger = convertToEntity(salesLedgerDto);
            salesLedger.setCustomerName(customer.getCustomerName());
            salesLedger.setTenantId(customer.getTenantId());
            // 3. 新增或更新主表
            if (salesLedger.getId() == null) {
                String contractNo = generateSalesContractNo();
                salesLedger.setSalesContractNo(contractNo);
                salesLedgerMapper.insert(salesLedger);
            } else {
                salesLedgerMapper.updateById(salesLedger);
            }
            // 4. 处理子表数据
            List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
            if (productList != null && !productList.isEmpty()) {
                handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
                updateMainContractAmount(
                        salesLedger.getId(),
                        productList,
                        SalesLedgerProduct::getTaxInclusiveTotalPrice,
                        salesLedgerMapper,
                        SalesLedger.class
                );
            }
            // 5. 迁移临时文件到正式目录
            if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
                migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
            }
            return 1;
        } catch (IOException e) {
            throw new BaseException("文件迁移失败: " + e.getMessage());
        // 1. 校验客户信息
        Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
        if (customer == null) {
            throw new BaseException("客户不存在");
        }
        // 2. DTO转Entity
        SalesLedger salesLedger = convertToEntity(salesLedgerDto);
        salesLedger.setCustomerName(customer.getCustomerName());
        salesLedger.setTenantId(customer.getTenantId());
        // 3. 新增或更新主表
        if (salesLedger.getId() == null) {
            String contractNo = generateSalesContractNo();
            salesLedger.setSalesContractNo(contractNo);
            salesLedgerMapper.insert(salesLedger);
        } else {
            salesLedgerMapper.updateById(salesLedger);
        }
        // 4. 处理子表数据
        List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
        if (productList != null && !productList.isEmpty()) {
            handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
            updateMainContractAmount(
                    salesLedger.getId(),
                    productList,
                    SalesLedgerProduct::getTaxInclusiveTotalPrice,
                    salesLedgerMapper,
                    SalesLedger.class
            );
        }
        //业务和附件绑定
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
//            if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
//                migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
//            }
        return 1;
    }
    /**
     * 将临时文件迁移到正式目录
     *
     * @param businessId  业务ID(销售台账ID)
     * @param tempFileIds 临时文件ID列表
     * @throws IOException 文件操作异常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // 构建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // 确保正式目录存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // 查询临时文件记录
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // 构建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // 执行文件迁移(使用原子操作确保安全性)
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // 原子移动失败,使用复制+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // 更新文件记录(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                //销售
                fileRecord.setType(FileNameType.SALE.getValue());
                commonFileMapper.insert(fileRecord);
                // 删除临时文件记录
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // 可选择回滚事务或记录失败文件
                throw new IOException("文件迁移异常", e);
            }
        }
    }
    // 文件迁移方法
    @Override
    public void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products, SaleEnum type) {