package com.ruoyi.sales.service.impl; 
 | 
  
 | 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 
 | 
import com.baomidou.mybatisplus.core.mapper.BaseMapper; 
 | 
import com.baomidou.mybatisplus.core.metadata.IPage; 
 | 
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; 
 | 
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; 
 | 
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.account.pojo.AccountExpense; 
 | 
import com.ruoyi.account.pojo.AccountIncome; 
 | 
import com.ruoyi.account.service.AccountIncomeService; 
 | 
import com.ruoyi.basic.mapper.CustomerMapper; 
 | 
import com.ruoyi.basic.pojo.Customer; 
 | 
import com.ruoyi.common.enums.FileNameType; 
 | 
import com.ruoyi.common.exception.base.BaseException; 
 | 
import com.ruoyi.common.utils.SecurityUtils; 
 | 
import com.ruoyi.common.utils.StringUtils; 
 | 
import com.ruoyi.other.mapper.TempFileMapper; 
 | 
import com.ruoyi.other.pojo.TempFile; 
 | 
import com.ruoyi.production.mapper.SalesLedgerSchedulingMapper; 
 | 
import com.ruoyi.production.pojo.SalesLedgerScheduling; 
 | 
import com.ruoyi.project.system.domain.SysDept; 
 | 
import com.ruoyi.project.system.mapper.SysDeptMapper; 
 | 
import com.ruoyi.sales.dto.MonthlyAmountDto; 
 | 
import com.ruoyi.sales.dto.SalesLedgerDto; 
 | 
import com.ruoyi.sales.mapper.*; 
 | 
import com.ruoyi.sales.pojo.*; 
 | 
import com.ruoyi.sales.service.ISalesLedgerService; 
 | 
import lombok.RequiredArgsConstructor; 
 | 
import lombok.extern.slf4j.Slf4j; 
 | 
import org.apache.commons.io.FilenameUtils; 
 | 
import org.springframework.beans.BeanUtils; 
 | 
import org.springframework.beans.factory.annotation.Autowired; 
 | 
import org.springframework.beans.factory.annotation.Value; 
 | 
import org.springframework.data.redis.core.RedisTemplate; 
 | 
import org.springframework.data.redis.core.script.DefaultRedisScript; 
 | 
import org.springframework.stereotype.Service; 
 | 
import org.springframework.transaction.annotation.Transactional; 
 | 
  
 | 
import java.io.IOException; 
 | 
import java.lang.reflect.Field; 
 | 
import java.math.BigDecimal; 
 | 
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; 
 | 
import java.time.format.DateTimeFormatter; 
 | 
import java.util.*; 
 | 
import java.util.concurrent.TimeUnit; 
 | 
import java.util.function.Function; 
 | 
import java.util.stream.Collectors; 
 | 
  
 | 
/** 
 | 
 * 销售台账Service业务层处理 
 | 
 * 
 | 
 * @author ruoyi 
 | 
 * @date 2025-05-08 
 | 
 */ 
 | 
@Service 
 | 
@RequiredArgsConstructor 
 | 
@Slf4j 
 | 
public class SalesLedgerServiceImpl extends ServiceImpl<SalesLedgerMapper, SalesLedger> implements ISalesLedgerService { 
 | 
    private final AccountIncomeService accountIncomeService; 
 | 
  
 | 
    private final SalesLedgerMapper salesLedgerMapper; 
 | 
  
 | 
    private final CustomerMapper customerMapper; 
 | 
  
 | 
    private final SalesLedgerProductMapper salesLedgerProductMapper; 
 | 
  
 | 
    private final CommonFileMapper commonFileMapper; 
 | 
  
 | 
    private final TempFileMapper tempFileMapper; 
 | 
  
 | 
    private final ReceiptPaymentMapper receiptPaymentMapper; 
 | 
  
 | 
    private final InvoiceLedgerMapper invoiceLedgerMapper; 
 | 
  
 | 
    private final SalesLedgerSchedulingMapper salesLedgerSchedulingMapper; 
 | 
  
 | 
    @Autowired 
 | 
    private SysDeptMapper sysDeptMapper; 
 | 
  
 | 
    @Value("${file.upload-dir}") 
 | 
    private String uploadDir; 
 | 
  
 | 
    private static final String LOCK_PREFIX = "contract_no_lock:"; 
 | 
    private static final long LOCK_WAIT_TIMEOUT = 10; // 锁等待超时时间(秒) 
 | 
    private static final long LOCK_EXPIRE_TIME = 30;  // 锁自动过期时间(秒) 
 | 
  
 | 
    private final RedisTemplate<String, String> redisTemplate; 
 | 
  
 | 
    @Override 
 | 
    public List<SalesLedger> selectSalesLedgerList(SalesLedgerDto salesLedgerDto) { 
 | 
        return salesLedgerMapper.selectSalesLedgerList(salesLedgerDto); 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public SalesLedgerDto getSalesLedgerWithProducts(SalesLedgerDto salesLedgerDto) { 
 | 
        // 1. 查询主表 
 | 
        SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerDto.getId()); 
 | 
        if (salesLedger == null) { 
 | 
            throw new BaseException("台账不存在"); 
 | 
        } 
 | 
  
 | 
        // 2. 查询子表 
 | 
        LambdaQueryWrapper<SalesLedgerProduct> productWrapper = new LambdaQueryWrapper<>(); 
 | 
        productWrapper.eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId()); 
 | 
        productWrapper.eq(SalesLedgerProduct::getType, 1); 
 | 
        List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(productWrapper); 
 | 
        for (SalesLedgerProduct product : products) { 
 | 
            product.setOriginalNoInvoiceNum(product.getNoInvoiceNum()); 
 | 
            // 提供临时未开票数,未开票金额供前段计算 
 | 
            product.setTempnoInvoiceAmount(product.getNoInvoiceAmount()); 
 | 
            product.setTempNoInvoiceNum(product.getNoInvoiceNum()); 
 | 
            product.setRegister(SecurityUtils.getLoginUser().getUser().getNickName()); 
 | 
            product.setRegisterDate(LocalDateTime.now()); 
 | 
        } 
 | 
  
 | 
        // 3.查询上传文件 
 | 
        LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>(); 
 | 
        salesLedgerFileWrapper.eq(CommonFile::getCommonId, salesLedger.getId()); 
 | 
        List<CommonFile> salesLedgerFiles = commonFileMapper.selectList(salesLedgerFileWrapper); 
 | 
  
 | 
        // 4. 转换 DTO 
 | 
        SalesLedgerDto resultDto = new SalesLedgerDto(); 
 | 
        BeanUtils.copyProperties(salesLedger, resultDto); 
 | 
        if (!products.isEmpty()) { 
 | 
            resultDto.setHasChildren(true); 
 | 
            resultDto.setProductData(products); 
 | 
            resultDto.setSalesLedgerFiles(salesLedgerFiles); 
 | 
        } 
 | 
        return resultDto; 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public List<Map<String, Object>> getSalesNo() { 
 | 
        LambdaQueryWrapper<SalesLedger> queryWrapper = Wrappers.lambdaQuery(); 
 | 
        queryWrapper.select(SalesLedger::getId, SalesLedger::getSalesContractNo, SalesLedger::getProjectName); 
 | 
  
 | 
        // 获取原始查询结果 
 | 
        List<Map<String, Object>> result = salesLedgerMapper.selectMaps(queryWrapper); 
 | 
  
 | 
        // 将下划线命名转换为驼峰命名 
 | 
        return result.stream().map(map -> map.entrySet().stream() 
 | 
                .collect(Collectors.toMap( 
 | 
                        entry -> underlineToCamel(entry.getKey()), 
 | 
                        Map.Entry::getValue)) 
 | 
        ).collect(Collectors.toList()); 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public BigDecimal getContractAmount() { 
 | 
        LocalDate now = LocalDate.now(); 
 | 
        YearMonth currentMonth = YearMonth.from(now); 
 | 
  
 | 
        // 创建LambdaQueryWrapper 
 | 
        LambdaQueryWrapper<SalesLedger> queryWrapper = new LambdaQueryWrapper<>(); 
 | 
        queryWrapper.ge(SalesLedger::getEntryDate, currentMonth.atDay(1).atStartOfDay())  // 大于等于本月第一天 
 | 
                .lt(SalesLedger::getEntryDate, currentMonth.plusMonths(1).atDay(1).atStartOfDay()); // 小于下月第一天 
 | 
  
 | 
        // 执行查询并计算总和 
 | 
        List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(queryWrapper); 
 | 
  
 | 
        BigDecimal totalContractAmount = salesLedgers.stream() 
 | 
                .map(SalesLedger::getContractAmount) 
 | 
                .filter(Objects::nonNull) 
 | 
                .reduce(BigDecimal.ZERO, BigDecimal::add); 
 | 
  
 | 
        return totalContractAmount; 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public List getTopFiveList() { 
 | 
        // 查询原始数据 
 | 
        LambdaQueryWrapper<SalesLedger> queryWrapper = Wrappers.lambdaQuery(); 
 | 
        queryWrapper.select(SalesLedger::getCustomerId, 
 | 
                        SalesLedger::getCustomerName, 
 | 
                        SalesLedger::getContractAmount) 
 | 
                .orderByDesc(SalesLedger::getContractAmount); 
 | 
        List<SalesLedger> records = salesLedgerMapper.selectList(queryWrapper); 
 | 
  
 | 
        // 按客户ID分组并聚合金额 
 | 
        Map<Long, GroupedCustomer> groupedMap = new LinkedHashMap<>(); // 使用LinkedHashMap保持排序 
 | 
        for (SalesLedger record : records) { 
 | 
            groupedMap.computeIfAbsent(record.getCustomerId(), 
 | 
                            k -> new GroupedCustomer(record.getCustomerId(), record.getCustomerName())) 
 | 
                    .addAmount(record.getContractAmount()); 
 | 
        } 
 | 
  
 | 
        // 转换为结果列表并取前5 
 | 
        return groupedMap.values().stream() 
 | 
                .sorted(Comparator.comparing(GroupedCustomer::getTotalAmount).reversed()) 
 | 
                .limit(5) 
 | 
                .map(customer -> { 
 | 
                    Map<String, Object> result = new HashMap<>(); 
 | 
                    result.put("customerId", customer.getCustomerId()); 
 | 
                    result.put("customerName", customer.getCustomerName()); 
 | 
                    result.put("totalAmount", customer.getTotalAmount()); 
 | 
                    return result; 
 | 
                }) 
 | 
                .collect(Collectors.toList()); 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public List<MonthlyAmountDto> getAmountHalfYear(Integer type) { 
 | 
        LocalDate now = LocalDate.now(); 
 | 
        LocalDateTime currentDateTime = LocalDateTime.now(); 
 | 
  
 | 
        // 根据 type 确定查询的时间间隔(天数)和总查询天数 
 | 
        int daysPerPeriod; 
 | 
        int totalDays; 
 | 
        switch (type) { 
 | 
            case 1: 
 | 
                daysPerPeriod = 5;   // 每5天查一次 
 | 
                totalDays = 30;       // 6次 × 5天 = 30天 
 | 
                break; 
 | 
            case 2: 
 | 
                daysPerPeriod = 15;    // 每15天查一次 
 | 
                totalDays = 90;       // 6次 × 15天 = 90天 
 | 
                break; 
 | 
            case 3: 
 | 
                daysPerPeriod = 30;   // 每30天查一次 
 | 
                totalDays = 180;      // 6次 × 30天 = 180天 
 | 
                break; 
 | 
            default: 
 | 
                throw new IllegalArgumentException("Invalid type value: " + type); 
 | 
        } 
 | 
  
 | 
        List<MonthlyAmountDto> result = new ArrayList<>(); 
 | 
  
 | 
        // 循环6次,每次查询一个时间段的数据 
 | 
        for (int i = 0; i < 6; i++) { 
 | 
            // 计算当前时间段的起始和结束时间 
 | 
            LocalDateTime endTime = currentDateTime.minusDays(i * daysPerPeriod); 
 | 
            LocalDateTime startTime = endTime.minusDays(daysPerPeriod); 
 | 
  
 | 
            // 查询回款金额 
 | 
            LambdaQueryWrapper<ReceiptPayment> receiptPaymentQuery = new LambdaQueryWrapper<>(); 
 | 
            receiptPaymentQuery 
 | 
                    .ge(ReceiptPayment::getCreateTime, startTime) 
 | 
                    .lt(ReceiptPayment::getCreateTime, endTime); 
 | 
            List<ReceiptPayment> receiptPayments = receiptPaymentMapper.selectList(receiptPaymentQuery); 
 | 
  
 | 
            // 查询开票金额 
 | 
            LambdaQueryWrapper<InvoiceLedger> invoiceLedgerQuery = new LambdaQueryWrapper<>(); 
 | 
            invoiceLedgerQuery 
 | 
                    .ge(InvoiceLedger::getCreateTime, startTime) 
 | 
                    .lt(InvoiceLedger::getCreateTime, endTime); 
 | 
            List<InvoiceLedger> invoiceLedgers = invoiceLedgerMapper.selectList(invoiceLedgerQuery); 
 | 
  
 | 
            // 计算回款总额 
 | 
            BigDecimal receiptAmount = receiptPayments.stream() 
 | 
                    .map(ReceiptPayment::getReceiptPaymentAmount) 
 | 
                    .filter(Objects::nonNull) 
 | 
                    .reduce(BigDecimal.ZERO, BigDecimal::add); 
 | 
  
 | 
            // 计算开票总额 
 | 
            BigDecimal invoiceAmount = invoiceLedgers.stream() 
 | 
                    .map(InvoiceLedger::getInvoiceTotal) 
 | 
                    .filter(Objects::nonNull) 
 | 
                    .reduce(BigDecimal.ZERO, BigDecimal::add); 
 | 
  
 | 
            // 构造返回的 DTO 
 | 
            MonthlyAmountDto dto = new MonthlyAmountDto(); 
 | 
            dto.setMonth(startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " ~ " + 
 | 
                    endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); 
 | 
            dto.setReceiptAmount(receiptAmount); 
 | 
            dto.setInvoiceAmount(invoiceAmount); 
 | 
  
 | 
            result.add(dto); 
 | 
        } 
 | 
  
 | 
        // 反转列表,使时间顺序从早到晚 
 | 
        Collections.reverse(result); 
 | 
        return result; 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    public IPage<SalesLedger> selectSalesLedgerListPage(Page page, SalesLedgerDto salesLedgerDto) { 
 | 
        return salesLedgerMapper.selectSalesLedgerListPage(page, salesLedgerDto); 
 | 
    } 
 | 
  
 | 
    // 内部类用于存储聚合结果 
 | 
    private static class GroupedCustomer { 
 | 
        private final Long customerId; 
 | 
        private final String customerName; 
 | 
        private BigDecimal totalAmount = BigDecimal.ZERO; 
 | 
  
 | 
        public GroupedCustomer(Long customerId, String customerName) { 
 | 
            this.customerId = customerId; 
 | 
            this.customerName = customerName; 
 | 
        } 
 | 
  
 | 
        public void addAmount(BigDecimal amount) { 
 | 
            if (amount != null) { 
 | 
                this.totalAmount = this.totalAmount.add(amount); 
 | 
            } 
 | 
        } 
 | 
  
 | 
        public Long getCustomerId() { 
 | 
            return customerId; 
 | 
        } 
 | 
  
 | 
        public String getCustomerName() { 
 | 
            return customerName; 
 | 
        } 
 | 
  
 | 
        public BigDecimal getTotalAmount() { 
 | 
            return totalAmount; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 下划线命名转驼峰命名 
 | 
     */ 
 | 
    private String underlineToCamel(String param) { 
 | 
        if (param == null || "".equals(param.trim())) { 
 | 
            return ""; 
 | 
        } 
 | 
        int len = param.length(); 
 | 
        StringBuilder sb = new StringBuilder(len); 
 | 
        for (int i = 0; i < len; i++) { 
 | 
            char c = param.charAt(i); 
 | 
            if (c == '_') { 
 | 
                if (++i < len) { 
 | 
                    sb.append(Character.toUpperCase(param.charAt(i))); 
 | 
                } 
 | 
            } else { 
 | 
                sb.append(Character.toLowerCase(c)); 
 | 
            } 
 | 
        } 
 | 
        return sb.toString(); 
 | 
    } 
 | 
  
 | 
    @Override 
 | 
    @Transactional(rollbackFor = Exception.class) 
 | 
    public int deleteSalesLedgerByIds(Long[] ids) { 
 | 
        List<Long> idList = Arrays.stream(ids) 
 | 
                .filter(Objects::nonNull) 
 | 
                .collect(Collectors.toList()); 
 | 
        if (CollectionUtils.isEmpty(idList)) { 
 | 
            return 0; 
 | 
        } 
 | 
        // 生产订单有待排产数据,台账不可删除 
 | 
        LambdaQueryWrapper<SalesLedgerScheduling> salesLedgerSchedulingLambdaQueryWrapper = new LambdaQueryWrapper<SalesLedgerScheduling>() 
 | 
                .in(SalesLedgerScheduling::getSalesLedgerId, idList); 
 | 
        if (salesLedgerSchedulingMapper.selectCount(salesLedgerSchedulingLambdaQueryWrapper) > 0) { 
 | 
            throw new BaseException("有排产数据,不可删除"); 
 | 
        } 
 | 
        // 1. 先删除子表数据 
 | 
        LambdaQueryWrapper<SalesLedgerProduct> productWrapper = new LambdaQueryWrapper<>(); 
 | 
        productWrapper.in(SalesLedgerProduct::getSalesLedgerId, idList); 
 | 
        salesLedgerProductMapper.delete(productWrapper); 
 | 
        // 删除生产订单数据 
 | 
        LambdaQueryWrapper<SalesLedgerScheduling> in = new LambdaQueryWrapper<SalesLedgerScheduling>() 
 | 
                .in(SalesLedgerScheduling::getSalesLedgerId, idList); 
 | 
        salesLedgerSchedulingMapper.delete(in); 
 | 
        // 2. 再删除主表数据 
 | 
        return salesLedgerMapper.deleteBatchIds(idList); 
 | 
    } 
 | 
  
 | 
    @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()); 
 | 
  
 | 
            AccountIncome accountIncome = new AccountIncome(); 
 | 
            accountIncome.setIncomeDate(salesLedger.getEntryDate()); 
 | 
            accountIncome.setIncomeType("0"); 
 | 
            accountIncome.setCustomerName(customer.getCustomerName()); 
 | 
            accountIncome.setIncomeMoney(salesLedger.getContractAmount()); 
 | 
            accountIncome.setIncomeMethod("0"); 
 | 
            accountIncome.setInputTime(new Date()); 
 | 
            accountIncome.setInputUser(salesLedger.getEntryPerson()); 
 | 
            // 3. 新增或更新主表 
 | 
            if (salesLedger.getId() == null) { 
 | 
                String contractNo = generateSalesContractNo(); 
 | 
                salesLedger.setSalesContractNo(contractNo); 
 | 
                salesLedgerMapper.insert(salesLedger); 
 | 
                accountIncome.setIncomeDescribed("销售合同:" + salesLedger.getSalesContractNo()); 
 | 
                accountIncome.setInvoiceNumber(salesLedger.getSalesContractNo()); 
 | 
                accountIncomeService.save(accountIncome); 
 | 
            } else { 
 | 
                salesLedgerMapper.updateById(salesLedger); 
 | 
                SalesLedger salesLedgerDB = salesLedgerMapper.selectById(salesLedger.getId()); 
 | 
                AccountIncome accountIncomeDB = accountIncomeService.getByInvoiceNumber(salesLedger.getSalesContractNo()); 
 | 
                if (ObjectUtils.isEmpty(accountIncomeDB)) { 
 | 
                    throw new BaseException("收入管理无该销售合同的收入记录"); 
 | 
                } 
 | 
                accountIncomeDB.setCustomerName(salesLedgerDB.getCustomerName()); 
 | 
                accountIncomeDB.setIncomeMoney(salesLedgerDB.getContractAmount()); 
 | 
                accountIncomeDB.setIncomeDescribed("销售合同:" + salesLedgerDB.getSalesContractNo()); 
 | 
                accountIncomeDB.setInvoiceNumber(salesLedgerDB.getSalesContractNo()); 
 | 
                accountIncomeDB.setInputTime(new Date()); 
 | 
                accountIncomeDB.setInputUser(salesLedgerDB.getEntryPerson()); 
 | 
                accountIncomeService.updateById(accountIncomeDB); 
 | 
            } 
 | 
  
 | 
            // 4. 处理子表数据 
 | 
            List<SalesLedgerProduct> productList = salesLedgerDto.getProductData(); 
 | 
            if (productList != null && !productList.isEmpty()) { 
 | 
                handleSalesLedgerProducts(salesLedger.getId(), productList, 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()); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    // 文件迁移方法 
 | 
  
 | 
    /** 
 | 
     * 将临时文件迁移到正式目录 
 | 
     * 
 | 
     * @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); 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
  
 | 
  
 | 
    private void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products, Integer type) { 
 | 
        // 按ID分组,区分新增和更新的记录 
 | 
        Map<Boolean, List<SalesLedgerProduct>> partitionedProducts = products.stream() 
 | 
                .peek(p -> p.setSalesLedgerId(salesLedgerId)) 
 | 
                .collect(Collectors.partitioningBy(p -> p.getId() != null)); 
 | 
  
 | 
        List<SalesLedgerProduct> updateList = partitionedProducts.get(true); 
 | 
        List<SalesLedgerProduct> insertList = partitionedProducts.get(false); 
 | 
  
 | 
        // 执行更新操作 
 | 
        if (!updateList.isEmpty()) { 
 | 
            for (SalesLedgerProduct product : updateList) { 
 | 
                product.setType(type); 
 | 
                salesLedgerProductMapper.updateById(product); 
 | 
            } 
 | 
        } 
 | 
        // 执行插入操作 
 | 
        if (!insertList.isEmpty()) { 
 | 
            for (SalesLedgerProduct salesLedgerProduct : insertList) { 
 | 
                salesLedgerProduct.setType(type); 
 | 
                salesLedgerProduct.setNoInvoiceNum(salesLedgerProduct.getQuantity()); 
 | 
                salesLedgerProduct.setNoInvoiceAmount(salesLedgerProduct.getTaxInclusiveTotalPrice()); 
 | 
                salesLedgerProductMapper.insert(salesLedgerProduct); 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
  
 | 
    private SalesLedger convertToEntity(SalesLedgerDto dto) { 
 | 
        SalesLedger entity = new SalesLedger(); 
 | 
        BeanUtils.copyProperties(dto, entity); 
 | 
        return entity; 
 | 
    } 
 | 
  
 | 
    @Transactional(readOnly = true) 
 | 
    public String generateSalesContractNo() { 
 | 
        LocalDate currentDate = LocalDate.now(); 
 | 
        String datePart = currentDate.format(DateTimeFormatter.BASIC_ISO_DATE); 
 | 
        String lockKey = LOCK_PREFIX + datePart; 
 | 
        String lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); // 唯一标识锁持有者 
 | 
  
 | 
        try { 
 | 
            // 1. 尝试获取分布式锁(循环直到超时) 
 | 
            long startWaitTime = System.currentTimeMillis(); 
 | 
            while (System.currentTimeMillis() - startWaitTime < LOCK_WAIT_TIMEOUT * 1000) { 
 | 
                // SET key value NX PX 30000:仅当锁不存在时获取,设置30秒过期 
 | 
                Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TimeUnit.SECONDS); 
 | 
                if (Boolean.TRUE.equals(locked)) { 
 | 
                    break; // 成功获取锁 
 | 
                } 
 | 
                // 短暂休眠避免忙等待 
 | 
                try { 
 | 
                    Thread.sleep(100); 
 | 
                } catch (InterruptedException e) { 
 | 
                    Thread.currentThread().interrupt(); 
 | 
                    throw new RuntimeException("获取锁时被中断", e); 
 | 
                } 
 | 
            } 
 | 
  
 | 
            if (!redisTemplate.hasKey(lockKey)) { 
 | 
                throw new RuntimeException("获取合同编号生成锁失败:超时"); 
 | 
            } 
 | 
  
 | 
            // 2. 查询当天/公司已存在的序列号(与原逻辑一致) 
 | 
            Long tenantId = SecurityUtils.getLoginUser().getTenantId(); 
 | 
            if (null != tenantId) { 
 | 
                //获取公司编号 
 | 
                SysDept sysDept = sysDeptMapper.selectDeptById(tenantId.longValue()); 
 | 
                if (!ObjectUtils.isEmpty(sysDept)) { 
 | 
                    datePart = (StringUtils.isEmpty(sysDept.getDeptNick()) ? "" : sysDept.getDeptNick()) + datePart; 
 | 
                } 
 | 
            } 
 | 
            List<Integer> existingSequences = salesLedgerMapper.selectSequencesByDate(datePart); 
 | 
            int nextSequence = findFirstMissingSequence(existingSequences); 
 | 
  
 | 
            return datePart + String.format("%03d", nextSequence); 
 | 
        } finally { 
 | 
            // 3. 释放锁(使用Lua脚本保证原子性,避免误删其他线程的锁) 
 | 
            String luaScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; 
 | 
            redisTemplate.execute( 
 | 
                    new DefaultRedisScript<>(luaScript, Long.class), 
 | 
                    Collections.singletonList(lockKey), 
 | 
                    lockValue // 只有持有相同值的线程才能删除锁 
 | 
            ); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    private int findFirstMissingSequence(List<Integer> sequences) { 
 | 
        if (sequences.isEmpty()) { 
 | 
            return 1; 
 | 
        } 
 | 
        // 排序后查找第一个缺失的正整数(与原逻辑一致) 
 | 
        sequences.sort(Integer::compareTo); 
 | 
        int next = 1; 
 | 
        for (int seq : sequences) { 
 | 
            if (seq == next) { 
 | 
                next++; 
 | 
            } else if (seq > next) { 
 | 
                break; 
 | 
            } 
 | 
        } 
 | 
        return next; 
 | 
    } 
 | 
  
 | 
    public <T, S> void updateMainContractAmount( 
 | 
            Long mainId, 
 | 
            List<T> subList, 
 | 
            Function<T, BigDecimal> amountGetter, 
 | 
            BaseMapper<S> mainMapper, 
 | 
            Class<S> mainEntityClass) { 
 | 
  
 | 
        if (mainId == null || subList == null || subList.isEmpty()) { 
 | 
            return; 
 | 
        } 
 | 
  
 | 
        // 计算子表金额总和 
 | 
        BigDecimal totalAmount = subList.stream() 
 | 
                .map(amountGetter) 
 | 
                .filter(Objects::nonNull) 
 | 
                .reduce(BigDecimal.ZERO, BigDecimal::add); 
 | 
  
 | 
        // 构造主表更新对象(支持任意主表类型) 
 | 
        try { 
 | 
            S entity = mainEntityClass.getDeclaredConstructor().newInstance(); 
 | 
            Field idField = mainEntityClass.getDeclaredField("id"); 
 | 
            idField.setAccessible(true); 
 | 
            idField.set(entity, mainId); 
 | 
  
 | 
            // 设置 contractAmount 字段,注意这里假设字段名为 "contractAmount" 
 | 
            Field amountField = mainEntityClass.getDeclaredField("contractAmount"); 
 | 
            amountField.setAccessible(true); 
 | 
            amountField.set(entity, totalAmount); 
 | 
  
 | 
            mainMapper.updateById(entity); 
 | 
        } catch (Exception e) { 
 | 
            throw new RuntimeException("动态更新主表金额失败", e); 
 | 
        } 
 | 
    } 
 | 
} 
 |