gongchunyi
8 小时以前 93daa0916a6d76275886e704f6735cb91c3baf4e
feat: 生产成本核算功能接口
已添加4个文件
已修改6个文件
750 ■■■■■ 文件已修改
doc/宁夏-中盛建材.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionSettlementBatchesController.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionSettlementDetailsDto.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionSettlementDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionSettlementTotalDto.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/enums/ProductionSettlementEnum.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionSettlementBatches.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionSettlementDetails.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/IProductionSettlementBatchesService.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionSettlementBatchesServiceImpl.java 466 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/ÄþÏÄ-ÖÐÊ¢½¨²Ä.sql
@@ -510,4 +510,22 @@
    KEY `idx_batch_id` (`batch_id`),
    KEY `idx_product_id` (`product_id`) COMMENT '方便按产品查询历史成本对比'
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4 COMMENT ='生产成本核算对比明细表';
  DEFAULT CHARSET = utf8mb4 COMMENT ='生产成本核算对比明细表';
ALTER TABLE `product-inventory-management-zsjc`.`production_settlement_batches`
    DROP INDEX `idx_period`;
ALTER TABLE `product-inventory-management-zsjc`.`production_settlement_batches`
    DROP COLUMN `batch_name`,
    DROP COLUMN `status`;
ALTER TABLE `product-inventory-management-zsjc`.`production_settlement_details`
    DROP COLUMN `actual_qty`,
    DROP COLUMN `actual_price`,
    DROP COLUMN `actual_total`,
    DROP COLUMN `diff_qty`,
    DROP COLUMN `diff_price`,
    DROP COLUMN `diff_total`;
ALTER TABLE `product-inventory-management-zsjc`.`production_settlement_batches`
    MODIFY COLUMN `period_time` varchar(255) NULL DEFAULT NULL COMMENT '核算归属年月' AFTER `id`;
src/main/java/com/ruoyi/production/controller/ProductionSettlementBatchesController.java
@@ -1,19 +1,22 @@
package com.ruoyi.production.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
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.service.IProductionSettlementBatchesService;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
 * <p>
@@ -33,9 +36,45 @@
    @PostMapping("/import")
    @ApiOperation("导入生产成本核算表")
    public AjaxResult importProductionSettlement(@RequestParam("file") MultipartFile file, @RequestParam(required = false) LocalDate periodTime, @RequestParam(required = false) String batchName) {
        productionSettlementBatchesService.importProductionSettlement(file, periodTime, batchName);
    public AjaxResult importProductionSettlement(@RequestParam("file") MultipartFile file, ProductionSettlementDto dto) {
        productionSettlementBatchesService.importProductionSettlement(file, dto);
        return AjaxResult.success();
    }
    @GetMapping("/downloadTemplate")
    @ApiOperation("生产成本核算导入模板")
    public void export(HttpServletResponse response) {
        ExcelUtil<SettlementImportDto> excelUtil = new ExcelUtil<>(SettlementImportDto.class);
        excelUtil.importTemplateExcel(response, "生产成本核算导入模板");
    }
    @GetMapping("/getSettlement")
    @ApiOperation("获取生产成本核算数据")
    public AjaxResult getSettlement(ProductionSettlementDto dto) {
        Map<String, List<ProductionSettlementDetailsDto>> map = productionSettlementBatchesService.getSettlement(dto);
        return AjaxResult.success(map);
    }
    @GetMapping("/getProductTypes")
    @ApiOperation("获取对应月份导入的产品类别")
    public AjaxResult getProductTypes(ProductionSettlementDto dto) {
        List<String> list = productionSettlementBatchesService.getProductTypes(dto);
        return AjaxResult.success(list);
    }
    @GetMapping("/getSubjectNames")
    @ApiOperation("获取对应月份导入的科目类别")
    public AjaxResult getSubjectNames(ProductionSettlementDto dto) {
        List<String> list = productionSettlementBatchesService.getSubjectNames(dto);
        return AjaxResult.success(list);
    }
    @GetMapping("/getTotalCosts")
    @ApiOperation("获取成本合计数据")
    public AjaxResult getTotalCosts(ProductionSettlementDto dto) {
        ProductionSettlementTotalDto totalCosts = productionSettlementBatchesService.getTotalCosts(dto);
        return AjaxResult.success(totalCosts);
    }
}
src/main/java/com/ruoyi/production/dto/ProductionSettlementDetailsDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.production.dto;
import com.ruoyi.production.pojo.ProductionSettlementDetails;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
 * <br>
 *
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/04/01 9:16
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductionSettlementDetailsDto extends ProductionSettlementDetails {
    @ApiModelProperty(value = "实际耗量")
    private BigDecimal actualQty;
    @ApiModelProperty(value = "实际单价")
    private BigDecimal actualPrice;
    @ApiModelProperty(value = "实际总成本")
    private BigDecimal actualTotal;
    @ApiModelProperty(value = "耗量差异")
    private String diffQty;
    @ApiModelProperty(value = "单价差异")
    private String diffPrice;
    @ApiModelProperty(value = "总成本差异")
    private String diffTotal;
}
src/main/java/com/ruoyi/production/dto/ProductionSettlementDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.production.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬æ ¸ç®—查询参数Dto
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/04/01
 */
@Data
@ApiModel(value = "ProductionSettlementDto", description = "生产成本核算查询参数Dto")
public class ProductionSettlementDto {
    @ApiModelProperty("核算成本日期")
        private String periodTime;
    @ApiModelProperty("核算产品类型")
    private String productType;
    @ApiModelProperty("核算成本科目")
    private String subjectName;
    @ApiModelProperty("核算成本类型")
    private String costType;
}
src/main/java/com/ruoyi/production/dto/ProductionSettlementTotalDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.production.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬æ ¸ç®—合计数据 Dto
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/04/01
 */
@Data
@ApiModel(value = "ProductionSettlementTotalDto", description = "生产成本核算合计数据")
public class ProductionSettlementTotalDto {
    @ApiModelProperty("标准成本合计")
    private BigDecimal budgetTotal;
    @ApiModelProperty("实际成本合计")
    private BigDecimal actualTotal;
    @ApiModelProperty("差异合计")
    private BigDecimal diffTotal;
    @ApiModelProperty("差异率")
    private String diffRate;
}
src/main/java/com/ruoyi/production/enums/ProductionSettlementEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.production.enums;
import lombok.Getter;
/**
 * <br>
 * äº§å“ç±»åž‹-科目名称
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/31
 */
@Getter
public enum ProductionSettlementEnum {
    MATERIAL_COST("材料成本", true),
    ENERGY_COST("能耗成本", false);
    private final String name;
    private final boolean requiresProduct;
    ProductionSettlementEnum(String name, boolean requiresProduct) {
        this.name = name;
        this.requiresProduct = requiresProduct;
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºææ–™æˆæœ¬
     *
     * @param name ç§‘目名称
     * @return boolean
     */
    public static boolean isMaterialCost(String name) {
        for (ProductionSettlementEnum e : values()) {
            if (e.getName().equals(name)) {
                return e.isRequiresProduct();
            }
        }
        return false;
    }
}
src/main/java/com/ruoyi/production/pojo/ProductionSettlementBatches.java
@@ -1,11 +1,11 @@
package com.ruoyi.production.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDate;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -24,7 +24,7 @@
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("production_settlement_batches")
@ApiModel(value="ProductionSettlementBatches对象", description="生产成本核算批次主表")
@ApiModel(value = "ProductionSettlementBatches对象", description = "生产成本核算批次主表")
public class ProductionSettlementBatches implements Serializable {
    private static final long serialVersionUID = 1L;
@@ -34,21 +34,17 @@
    private Long id;
    @ApiModelProperty(value = "核算归属月份")
    private LocalDate periodTime;
    @ApiModelProperty(value = "批次名称")
    private String batchName;
    @ApiModelProperty(value = "状态:0-仅预算,1-结算计算中,2-已完成结算,3-已锁定")
    private Integer status;
    private String periodTime;
    @ApiModelProperty(value = "导入用户")
    private String createUser;
    @ApiModelProperty(value = "创建日期")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty(value = "租户ID")
    @TableField(value = "tenant_id", fill = FieldFill.INSERT)
    private Long tenantId;
src/main/java/com/ruoyi/production/pojo/ProductionSettlementDetails.java
@@ -1,10 +1,11 @@
package com.ruoyi.production.pojo;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -23,7 +24,7 @@
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("production_settlement_details")
@ApiModel(value="ProductionSettlementDetails对象", description="生产成本核算对比明细表")
@ApiModel(value = "ProductionSettlementDetails对象", description = "生产成本核算对比明细表")
public class ProductionSettlementDetails implements Serializable {
    private static final long serialVersionUID = 1L;
@@ -56,26 +57,8 @@
    @ApiModelProperty(value = "预算总成本")
    private BigDecimal budgetTotal;
    @ApiModelProperty(value = "实际耗量")
    private BigDecimal actualQty;
    @ApiModelProperty(value = "实际单价")
    private BigDecimal actualPrice;
    @ApiModelProperty(value = "实际总成本")
    private BigDecimal actualTotal;
    @ApiModelProperty(value = "耗量差异")
    private BigDecimal diffQty;
    @ApiModelProperty(value = "单价差异")
    private BigDecimal diffPrice;
    @ApiModelProperty(value = "总成本差异")
    private BigDecimal diffTotal;
    @ApiModelProperty(value = "租户ID")
    @TableField(value = "tenant_id", fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/production/service/IProductionSettlementBatchesService.java
@@ -1,10 +1,14 @@
package com.ruoyi.production.service;
import com.ruoyi.production.dto.ProductionSettlementDetailsDto;
import com.ruoyi.production.dto.ProductionSettlementDto;
import com.ruoyi.production.dto.ProductionSettlementTotalDto;
import com.ruoyi.production.pojo.ProductionSettlementBatches;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
 * <p>
@@ -16,6 +20,13 @@
 */
public interface IProductionSettlementBatchesService extends IService<ProductionSettlementBatches> {
    void importProductionSettlement(MultipartFile file, LocalDate periodTime, String batchName);
    void importProductionSettlement(MultipartFile file, ProductionSettlementDto dto);
    Map<String, List<ProductionSettlementDetailsDto>> getSettlement(ProductionSettlementDto dto);
    List<String> getProductTypes(ProductionSettlementDto dto);
    List<String> getSubjectNames(ProductionSettlementDto dto);
    ProductionSettlementTotalDto getTotalCosts(ProductionSettlementDto dto);
}
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("解析Excel失败", e);
            throw new ServiceException("解析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("已清除 {} æœˆä»½çš„æ—§æ ¸ç®—数据,批次ID:{}", 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;
    }
}