package com.ruoyi.production.service.impl; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.energy.pojo.Energy; import com.ruoyi.energy.pojo.EnergyConsumptionDetail; import com.ruoyi.energy.service.EnergyConsumptionDetailService; import com.ruoyi.energy.service.EnergyService; import com.ruoyi.production.dto.ProductionSettlementDetailsDto; import com.ruoyi.production.dto.ProductionSettlementDto; import com.ruoyi.production.dto.ProductionSettlementTotalDto; import com.ruoyi.production.dto.SettlementImportDto; import com.ruoyi.production.enums.ProductionSettlementEnum; import com.ruoyi.production.pojo.*; import com.ruoyi.production.mapper.ProductionSettlementBatchesMapper; import com.ruoyi.production.service.*; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.project.system.domain.SysDictData; import com.ruoyi.project.system.mapper.SysDictDataMapper; import com.ruoyi.production.utils.UnitUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; /** *

* 生产成本核算批次主表 服务实现类 *

* * @author deslrey * @since 2026-03-30 */ @Service @Slf4j public class ProductionSettlementBatchesServiceImpl extends ServiceImpl implements IProductionSettlementBatchesService { @Autowired private IProductionSettlementDetailsService productionSettlementDetailsService; @Autowired private ProductMaterialService productMaterialService; @Autowired private ProductMaterialSkuService productMaterialSkuService; @Autowired private ProductionProductMainService productionProductMainService; @Autowired private ProductionProductInputService productionProductInputService; @Autowired private IProductionOrderStructureService productionOrderStructureService; @Autowired private ProductOrderService productOrderService; @Autowired private IProductionOrderRouteService productionOrderRouteService; @Autowired private EnergyService energyService; @Autowired private EnergyConsumptionDetailService energyConsumptionDetailService; @Autowired private SysDictDataMapper sysDictDataMapper; @Override @Transactional(rollbackFor = Exception.class) public void importProductionSettlement(MultipartFile file, ProductionSettlementDto dto) { if (file == null || file.isEmpty()) { throw new ServiceException("导入失败,文件不能为空"); } // 核算月份,如果不填则默认为当前月 String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null); List list; try { list = new ExcelUtil<>(SettlementImportDto.class).importExcel(file.getInputStream()); } catch (Exception e) { log.error("解析Excel失败", e); throw new ServiceException("解析Excel文件失败:" + e.getMessage()); } if (list == null || list.isEmpty()) { throw new ServiceException("导入数据不能为空!"); } // 如果该月批次已存在,先清除旧批次及其明细,再重新导入 ProductionSettlementBatches existBatch = this.lambdaQuery() .eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime) .one(); if (existBatch != null) { productionSettlementDetailsService.lambdaUpdate() .eq(ProductionSettlementDetails::getBatchId, existBatch.getId()) .remove(); this.removeById(existBatch.getId()); log.info("已清除 {} 月份的旧核算数据,批次ID:{}", finalPeriodTime, existBatch.getId()); } ProductionSettlementBatches batch = new ProductionSettlementBatches(); batch.setPeriodTime(finalPeriodTime); batch.setCreateUser(SecurityUtils.getUsername()); this.save(batch); String lastProductType = ""; String lastCategory = ""; List detailList = new ArrayList<>(); int rowIndex = 2; for (SettlementImportDto importDto : list) { ProductionSettlementDetails detail = new ProductionSettlementDetails(); detail.setBatchId(batch.getId()); if (StringUtils.isNotBlank(importDto.getProductType())) { lastProductType = importDto.getProductType(); } if (StringUtils.isNotBlank(importDto.getCategory())) { lastCategory = importDto.getCategory(); } detail.setProductType(lastProductType); detail.setCategory(lastCategory); detail.setSubjectName(importDto.getSubjectName()); detail.setBudgetQty(importDto.getBudgetQty() != null ? importDto.getBudgetQty() : BigDecimal.ZERO); detail.setBudgetPrice(importDto.getBudgetPrice() != null ? importDto.getBudgetPrice() : BigDecimal.ZERO); detail.setBudgetTotal(importDto.getBudgetTotal() != null ? importDto.getBudgetTotal() : BigDecimal.ZERO); if (ProductionSettlementEnum.isMaterialCost(detail.getCategory())) { ProductMaterial product = productMaterialService.lambdaQuery() .eq(ProductMaterial::getProductName, importDto.getSubjectName()) .one(); if (product != null) { detail.setProductId(product.getId()); } else { log.warn("第 {} 行,未找到科目产品:{}", rowIndex, importDto.getSubjectName()); } } detailList.add(detail); rowIndex++; } productionSettlementDetailsService.saveBatch(detailList); } @Override public Map> getSettlement(ProductionSettlementDto dto) { String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null); YearMonth yearMonth = YearMonth.parse(finalPeriodTime, DateTimeFormatter.ofPattern("yyyy-MM")); LocalDate date = yearMonth.atDay(1); ProductionSettlementBatches batch = this.lambdaQuery() .eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime) .one(); if (batch == null) { return new HashMap<>(); } String filterProductType = dto != null ? dto.getProductType() : null; String filterSubjectName = dto != null ? dto.getSubjectName() : null; String filterCostType = dto != null ? dto.getCostType() : null; List details = productionSettlementDetailsService.lambdaQuery() .eq(ProductionSettlementDetails::getBatchId, batch.getId()) .eq(StringUtils.isNotBlank(filterProductType), ProductionSettlementDetails::getProductType, filterProductType) .eq(StringUtils.isNotBlank(filterSubjectName), ProductionSettlementDetails::getSubjectName, filterSubjectName) .eq(StringUtils.isNotBlank(filterCostType), ProductionSettlementDetails::getCategory, filterCostType) .list(); List dtoList = details.stream().map(d -> { ProductionSettlementDetailsDto detailDto = new ProductionSettlementDetailsDto(); detailDto.setBatchId(d.getBatchId()); detailDto.setProductId(d.getProductId()); detailDto.setProductType(d.getProductType()); detailDto.setCategory(d.getCategory()); detailDto.setSubjectName(d.getSubjectName()); detailDto.setBudgetQty(d.getBudgetQty()); detailDto.setBudgetPrice(d.getBudgetPrice()); detailDto.setBudgetTotal(d.getBudgetTotal()); return detailDto; }).collect(Collectors.toList()); LocalDateTime start = date.atStartOfDay(); LocalDateTime end = start.plusMonths(1); // 获取产品分类 List sysDictDataList = sysDictDataMapper.selectDictDataByType("product_type"); Map dictCodeMap = sysDictDataList.stream() .collect(Collectors.toMap(SysDictData::getDictCode, SysDictData::getDictLabel, (k1, k2) -> k1)); // 获取物料消耗数据 List mains = productionProductMainService.lambdaQuery() .ge(ProductionProductMain::getCreateTime, start) .lt(ProductionProductMain::getCreateTime, end) .list(); List mainIds = mains.stream().map(ProductionProductMain::getId).collect(Collectors.toList()); List orderIds = mains.stream().map(ProductionProductMain::getProductOrderId).distinct().collect(Collectors.toList()); Map> inputsByMain = new HashMap<>(); Map orderMap = new HashMap<>(); Map orderRouteMap = new HashMap<>(); Map> structuresByOrder = new HashMap<>(); if (!mainIds.isEmpty()) { inputsByMain = productionProductInputService.lambdaQuery() .in(ProductionProductInput::getProductMainId, mainIds) .list() .stream() .collect(Collectors.groupingBy(ProductionProductInput::getProductMainId)); } if (!orderIds.isEmpty()) { orderMap = productOrderService.lambdaQuery() .in(ProductOrder::getId, orderIds) .list() .stream() .collect(Collectors.toMap(ProductOrder::getId, o -> o)); orderRouteMap = productionOrderRouteService.lambdaQuery() .in(ProductionOrderRoute::getOrderId, orderIds) .list() .stream() .collect(Collectors.toMap(ProductionOrderRoute::getOrderId, r -> r, (k1, k2) -> k1)); structuresByOrder = productionOrderStructureService.lambdaQuery() .in(ProductionOrderStructure::getOrderId, orderIds) .list() .stream() .collect(Collectors.groupingBy(ProductionOrderStructure::getOrderId)); } // 获取能耗数据 LocalDate startDate = date; LocalDate endDate = date.plusMonths(1); List energyDetails = energyConsumptionDetailService.lambdaQuery() .ge(EnergyConsumptionDetail::getMeterReadingDate, startDate) .lt(EnergyConsumptionDetail::getMeterReadingDate, endDate) .list(); List energies = energyService.list(); Map energyMap = new HashMap<>(); for (Energy energy : energies) { if (energy.getEnergyName() != null) { energyMap.put(energy.getEnergyName(), energy); } } List allSkus = productMaterialSkuService.list(); Map skuToMaterialMap = allSkus.stream() .filter(sku -> sku.getProductId() != null) .collect(Collectors.toMap(ProductMaterialSku::getId, ProductMaterialSku::getProductId, (k1, k2) -> k1)); // 计算实际值 for (ProductionSettlementDetailsDto detail : dtoList) { BigDecimal actualQty = BigDecimal.ZERO; BigDecimal actualTotalCost = BigDecimal.ZERO; if (ProductionSettlementEnum.isMaterialCost(detail.getCategory())) { if (detail.getProductId() != null) { for (ProductionProductMain main : mains) { ProductOrder order = orderMap.get(main.getProductOrderId()); if (order == null) continue; String targetType = detail.getProductType() != null ? detail.getProductType().trim() : ""; boolean typeMatch = "综合".equals(targetType); if (!typeMatch) { ProductionOrderRoute route = orderRouteMap.get(main.getProductOrderId()); if (route != null && route.getDictCode() != null) { String dictLabel = dictCodeMap.get(route.getDictCode()); if (dictLabel != null && dictLabel.trim().equals(targetType)) { typeMatch = true; } } } if (!typeMatch && order.getStrength() != null && order.getStrength().trim().equals(targetType)) { typeMatch = true; } if (typeMatch) { List mainInputs = inputsByMain.get(main.getId()); if (mainInputs != null) { for (ProductionProductInput input : mainInputs) { Long inputMaterialId = skuToMaterialMap.get(input.getProductId()); if (Objects.equals(inputMaterialId, detail.getProductId())) { BigDecimal rawQty = input.getQuantity() != null ? input.getQuantity() : BigDecimal.ZERO; List orderStructures = structuresByOrder.get(order.getId()); BigDecimal unitPrice = BigDecimal.ZERO; String unit = ""; if (orderStructures != null) { for (ProductionOrderStructure structure : orderStructures) { Long structureMaterialId = skuToMaterialMap.get(structure.getProductModelId()); if (Objects.equals(structureMaterialId, detail.getProductId())) { unitPrice = structure.getUnitPrice() != null ? structure.getUnitPrice() : BigDecimal.ZERO; unit = structure.getUnit(); break; } } } actualTotalCost = actualTotalCost.add(rawQty.multiply(unitPrice)); BigDecimal tonnage = UnitUtils.convertValueToTon(rawQty, unit); actualQty = actualQty.add(tonnage); } } } } } } detail.setActualQty(actualQty); detail.setActualTotal(actualTotalCost); if (actualQty.compareTo(BigDecimal.ZERO) > 0) { detail.setActualPrice(actualTotalCost.divide(actualQty, 15, RoundingMode.HALF_UP)); } else { detail.setActualPrice(BigDecimal.ZERO); } } else if ("能耗成本".equals(detail.getCategory())) { Energy energy = energyMap.get(detail.getSubjectName()); if (energy != null) { BigDecimal unitPrice = energy.getUnitPrice() != null ? energy.getUnitPrice() : BigDecimal.ZERO; for (EnergyConsumptionDetail eDetail : energyDetails) { if (Objects.equals(eDetail.getEnergyId(), energy.getId())) { actualQty = actualQty.add(eDetail.getDosage() != null ? eDetail.getDosage() : BigDecimal.ZERO); } } detail.setActualQty(actualQty); detail.setActualPrice(unitPrice); detail.setActualTotal(actualQty.multiply(unitPrice)); } else { detail.setActualQty(BigDecimal.ZERO); detail.setActualPrice(BigDecimal.ZERO); detail.setActualTotal(BigDecimal.ZERO); } } else { detail.setActualQty(BigDecimal.ZERO); detail.setActualPrice(BigDecimal.ZERO); detail.setActualTotal(BigDecimal.ZERO); } // 差异值百分比计算 detail.setDiffQty(calculatePercentage(detail.getActualQty(), detail.getBudgetQty())); detail.setDiffPrice(calculatePercentage(detail.getActualPrice(), detail.getBudgetPrice())); detail.setDiffTotal(calculatePercentage(detail.getActualTotal(), detail.getBudgetTotal())); } return dtoList.stream().collect(Collectors.groupingBy(ProductionSettlementDetailsDto::getCategory)); } @Override public List getProductTypes(ProductionSettlementDto dto) { String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null); ProductionSettlementBatches batch = this.lambdaQuery() .eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime) .one(); if (batch == null) { return new ArrayList<>(); } return productionSettlementDetailsService.lambdaQuery() .eq(ProductionSettlementDetails::getBatchId, batch.getId()) .list() .stream() .map(ProductionSettlementDetails::getProductType) .filter(StringUtils::isNotBlank) .distinct() .collect(Collectors.toList()); } @Override public List getSubjectNames(ProductionSettlementDto dto) { String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null); ProductionSettlementBatches batch = this.lambdaQuery() .eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime) .one(); if (batch == null) { return new ArrayList<>(); } return productionSettlementDetailsService.lambdaQuery() .eq(ProductionSettlementDetails::getBatchId, batch.getId()) .list() .stream() .map(ProductionSettlementDetails::getSubjectName) .filter(StringUtils::isNotBlank) .distinct() .collect(Collectors.toList()); } /** * 核算月份,为空时默认取当前月,格式为 yyyy-MM */ private String parsePeriodTime(String periodTime) { if (StringUtils.isBlank(periodTime)) { return YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); } try { return YearMonth.parse(periodTime, DateTimeFormatter.ofPattern("yyyy-MM")) .format(DateTimeFormatter.ofPattern("yyyy-MM")); } catch (Exception e) { throw new ServiceException("日期格式错误,请使用 yyyy-MM 格式"); } } private String calculatePercentage(BigDecimal actual, BigDecimal budget) { if (budget == null || budget.compareTo(BigDecimal.ZERO) == 0) { return actual.compareTo(BigDecimal.ZERO) > 0 ? "100%" : "0%"; } BigDecimal diff = actual.subtract(budget); BigDecimal percentage = diff.divide(budget, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")); return percentage.stripTrailingZeros().toPlainString() + "%"; } @Override public ProductionSettlementTotalDto getTotalCosts(ProductionSettlementDto dto) { // 复用 getSettlement 获取含实际值的明细列表 Map> settlementMap = getSettlement(dto); List allDetails = settlementMap.values().stream() .flatMap(List::stream) .collect(Collectors.toList()); // 按 dto 中的其余条件在内存中过滤 if (dto != null) { if (StringUtils.isNotBlank(dto.getProductType())) { String targetType = dto.getProductType().trim(); allDetails = allDetails.stream() .filter(d -> targetType.equals(d.getProductType() != null ? d.getProductType().trim() : "")) .collect(Collectors.toList()); } if (StringUtils.isNotBlank(dto.getSubjectName())) { String targetSubject = dto.getSubjectName().trim(); allDetails = allDetails.stream() .filter(d -> targetSubject.equals(d.getSubjectName() != null ? d.getSubjectName().trim() : "")) .collect(Collectors.toList()); } if (StringUtils.isNotBlank(dto.getCostType())) { String targetCostType = dto.getCostType().trim(); allDetails = allDetails.stream() .filter(d -> targetCostType.equals(d.getCategory() != null ? d.getCategory().trim() : "")) .collect(Collectors.toList()); } } // 汇总标准成本与实际成本 BigDecimal budgetTotal = allDetails.stream() .map(d -> d.getBudgetTotal() != null ? d.getBudgetTotal() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal actualTotal = allDetails.stream() .map(d -> d.getActualTotal() != null ? d.getActualTotal() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal diffTotal = actualTotal.subtract(budgetTotal); // 差异率:带正负号 String diffRate; if (budgetTotal.compareTo(BigDecimal.ZERO) == 0) { diffRate = actualTotal.compareTo(BigDecimal.ZERO) > 0 ? "+100%" : "0%"; } else { BigDecimal rate = diffTotal.divide(budgetTotal, 4, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")); String sign = rate.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; diffRate = sign + rate.stripTrailingZeros().toPlainString() + "%"; } ProductionSettlementTotalDto result = new ProductionSettlementTotalDto(); result.setBudgetTotal(budgetTotal); result.setActualTotal(actualTotal); result.setDiffTotal(diffTotal); result.setDiffRate(diffRate); return result; } }