From 8c6aad555647fcecaddfaed99516d8b93db837c8 Mon Sep 17 00:00:00 2001
From: zss <zss@example.com>
Date: 星期三, 01 四月 2026 11:18:33 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_宁夏_中盛建材' into dev_宁夏_中盛建材

---
 src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java |  466 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 437 insertions(+), 29 deletions(-)

diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java
index 7a6bcb2..b036f7e 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java
@@ -4,22 +4,35 @@
 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.pojo.ProductionSettlementBatches;
+import com.ruoyi.production.enums.ProductionSettlementEnum;
+import com.ruoyi.production.pojo.*;
 import com.ruoyi.production.mapper.ProductionSettlementBatchesMapper;
-import com.ruoyi.production.pojo.ProductionSettlementDetails;
-import com.ruoyi.production.service.IProductionSettlementBatchesService;
+import com.ruoyi.production.service.*;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.ruoyi.production.service.IProductionSettlementDetailsService;
+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.util.List;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -31,58 +44,453 @@
  * @since 2026-03-30
  */
 @Service
+@Slf4j
 public class ProductionSettlementBatchesServiceImpl extends ServiceImpl<ProductionSettlementBatchesMapper, ProductionSettlementBatches> 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, LocalDate periodTime, String batchName) {
+    public void importProductionSettlement(MultipartFile file, ProductionSettlementDto dto) {
         if (file == null || file.isEmpty()) {
             throw new ServiceException("瀵煎叆澶辫触锛屾枃浠朵笉鑳戒负绌�");
         }
+        // 鏍哥畻鏈堜唤锛屽鏋滀笉濉垯榛樿涓哄綋鍓嶆湀
+        String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null);
 
         List<SettlementImportDto> list;
         try {
-            ExcelUtil<SettlementImportDto> util = new ExcelUtil<>(SettlementImportDto.class);
-            list = util.importExcel(file.getInputStream());
+            list = new ExcelUtil<>(SettlementImportDto.class).importExcel(file.getInputStream());
         } catch (Exception e) {
-            log.error("瀵煎叆鐢熶骇鎴愭湰鏍哥畻澶辫触", e);
+            log.error("瑙f瀽Excel澶辫触", e);
             throw new ServiceException("瑙f瀽Excel鏂囦欢澶辫触锛�" + e.getMessage());
         }
 
-        if (StringUtils.isEmpty(list)) {
+        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("宸叉竻闄� {} 鏈堜唤鐨勬棫鏍哥畻鏁版嵁锛屾壒娆D锛歿}", finalPeriodTime, existBatch.getId());
+        }
+
         ProductionSettlementBatches batch = new ProductionSettlementBatches();
-        batch.setBatchName(StringUtils.isNotEmpty(batchName) ? batchName : periodTime + "鎴愭湰鏍哥畻瀵煎叆");
-        batch.setPeriodTime(periodTime != null ? periodTime : LocalDate.now());
-        batch.setStatus(0);
-        batch.setCreateTime(LocalDateTime.now());
+        batch.setPeriodTime(finalPeriodTime);
         batch.setCreateUser(SecurityUtils.getUsername());
         this.save(batch);
 
-        List<ProductionSettlementDetails> detailList = list.stream().map(dto -> {
+        String lastProductType = "";
+        String lastCategory = "";
+
+        List<ProductionSettlementDetails> detailList = new ArrayList<>();
+        int rowIndex = 2;
+        for (SettlementImportDto importDto : list) {
             ProductionSettlementDetails detail = new ProductionSettlementDetails();
             detail.setBatchId(batch.getId());
-            detail.setProductType(dto.getProductType());
-            detail.setCategory(dto.getCategory());
-            detail.setSubjectName(dto.getSubjectName());
-            detail.setBudgetQty(dto.getBudgetQty());
-            detail.setBudgetPrice(dto.getBudgetPrice());
-            detail.setBudgetTotal(dto.getBudgetTotal());
-            detail.setActualQty(BigDecimal.ZERO);
-            detail.setActualPrice(BigDecimal.ZERO);
-            detail.setActualTotal(BigDecimal.ZERO);
-            detail.setDiffQty(BigDecimal.ZERO);
-            detail.setDiffPrice(BigDecimal.ZERO);
-            detail.setDiffTotal(BigDecimal.ZERO);
 
-            return detail;
-        }).collect(Collectors.toList());
+            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<String, List<ProductionSettlementDetailsDto>> 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<ProductionSettlementDetails> 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<ProductionSettlementDetailsDto> 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<SysDictData> sysDictDataList = sysDictDataMapper.selectDictDataByType("product_type");
+        Map<Long, String> dictCodeMap = sysDictDataList.stream()
+                .collect(Collectors.toMap(SysDictData::getDictCode, SysDictData::getDictLabel, (k1, k2) -> k1));
+
+        // 鑾峰彇鐗╂枡娑堣�楁暟鎹�
+        List<ProductionProductMain> mains = productionProductMainService.lambdaQuery()
+                .ge(ProductionProductMain::getCreateTime, start)
+                .lt(ProductionProductMain::getCreateTime, end)
+                .list();
+
+        List<Long> mainIds = mains.stream().map(ProductionProductMain::getId).collect(Collectors.toList());
+        List<Long> orderIds = mains.stream().map(ProductionProductMain::getProductOrderId).distinct().collect(Collectors.toList());
+
+        Map<Long, List<ProductionProductInput>> inputsByMain = new HashMap<>();
+        Map<Long, ProductOrder> orderMap = new HashMap<>();
+        Map<Long, ProductionOrderRoute> orderRouteMap = new HashMap<>();
+        Map<Long, List<ProductionOrderStructure>> 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<EnergyConsumptionDetail> energyDetails = energyConsumptionDetailService.lambdaQuery()
+                .ge(EnergyConsumptionDetail::getMeterReadingDate, startDate)
+                .lt(EnergyConsumptionDetail::getMeterReadingDate, endDate)
+                .list();
+
+        List<Energy> energies = energyService.list();
+        Map<String, Energy> energyMap = new HashMap<>();
+        for (Energy energy : energies) {
+            if (energy.getEnergyName() != null) {
+                energyMap.put(energy.getEnergyName(), energy);
+            }
+        }
+
+        List<ProductMaterialSku> allSkus = productMaterialSkuService.list();
+        Map<Long, Long> 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<ProductionProductInput> 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<ProductionOrderStructure> 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<String> 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<String> 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<String, List<ProductionSettlementDetailsDto>> settlementMap = getSettlement(dto);
+
+        List<ProductionSettlementDetailsDto> 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;
+    }
 }
\ No newline at end of file

--
Gitblit v1.9.3