gongchunyi
9 小时以前 fe4d8530e45569a8b17886f4bd050e96e187fb8d
feat: 生产成品核算报表接口,生产订单绑定工艺路线
已添加16个文件
已修改3个文件
905 ■■■■■ 文件已修改
doc/宁夏-中盛建材.sql 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionCostAccountController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRouteController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/GroupKeyDto.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionCostAccountDto.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostMapper.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderRouteMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/IProductionOrderRouteService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionCostService.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionCostServiceImpl.java 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderAppendixServiceImpl.java 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRouteServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/utils/UnitUtils.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/vo/ProductionCostAggregationVo.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/vo/ProductionCostDetailVo.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/vo/ProductionCostSummaryVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductOrderMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostMapper.xml 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderRouteMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/ÄþÏÄ-ÖÐÊ¢½¨²Ä.sql
@@ -446,4 +446,26 @@
    DROP COLUMN `dict_code`;
ALTER TABLE `product-inventory-management-zsjc`.`production_product_output`
    ADD COLUMN `total_quantity` decimal(20, 15) NULL COMMENT '总数量' AFTER `scrap_qty`;
    ADD COLUMN `total_quantity` decimal(20, 15) NULL COMMENT '总数量' AFTER `scrap_qty`;
CREATE TABLE `production_order_route`
(
    `id`                 bigint                                                        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `order_id`           bigint                                                        NOT NULL COMMENT '关联生产订单ID (production_order.id)',
    `process_route_id`   bigint                                                                 DEFAULT NULL COMMENT '原始工艺路线ID (process_route.id)',
    `product_model_id`   bigint                                                                 DEFAULT '0' COMMENT '产品id',
    `process_route_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci          DEFAULT NULL COMMENT '工艺路线编码',
    `bom_id`             int                                                                    DEFAULT NULL COMMENT '关联bom的id',
    `dict_code`          bigint                                                        NOT NULL COMMENT '产品类型字典编码',
    `description`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '描述',
    `tenant_id`          bigint                                                        NOT NULL COMMENT '租户id',
    `create_by`          varchar(64)                                                            DEFAULT '' COMMENT '创建者',
    `create_time`        datetime                                                               DEFAULT NULL COMMENT '录入时间',
    `update_by`          varchar(64)                                                            DEFAULT '' COMMENT '更新者',
    `update_time`        datetime                                                               DEFAULT NULL COMMENT '更新时间',
    `remark`             varchar(500)                                                           DEFAULT NULL COMMENT '备注',
    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_order_id` (`order_id`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_0900_ai_ci COMMENT ='生产订单绑定的工艺路线表';
src/main/java/com/ruoyi/production/controller/ProductionCostAccountController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.production.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.production.dto.ProductionCostAccountDto;
import com.ruoyi.production.service.ProductionCostService;
import com.ruoyi.production.vo.ProductionCostAggregationVo;
import com.ruoyi.production.vo.ProductionCostDetailVo;
import com.ruoyi.production.vo.ProductionCostSummaryVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * <br>
 * æˆæœ¬æ ¸ç®—控制层
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30
 */
@RestController
@RequestMapping("/cost")
@ApiModel(value = "ProductionCostAccountControllerç±»", description = "成本核算控制层")
public class ProductionCostAccountController {
    @Autowired
    private ProductionCostService productionCostService;
    //****************************************************          ç”Ÿäº§æˆå“æ ¸ç®—         *****************************************************  //
    @GetMapping("/productionCost/summary")
    @ApiOperation("顶部统计卡片数据")
    public AjaxResult getCostSummary(ProductionCostAccountDto dto) {
        ProductionCostSummaryVo costSummary = productionCostService.getCostSummary(dto);
        return AjaxResult.success(costSummary);
    }
    @GetMapping("/productionCost/aggregate/product")
    @ApiOperation("按产品物料汇总(分页)")
    public AjaxResult getProductAggregation(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto) {
        IPage<ProductionCostAggregationVo> aggregation = productionCostService.getProductAggregationPage(page, dto);
        return AjaxResult.success(aggregation);
    }
    @GetMapping("/productionCost/aggregate/order")
    @ApiOperation("按生产订单汇总(分页)")
    public AjaxResult getOrderAggregation(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto) {
        IPage<ProductionCostAggregationVo> aggregation = productionCostService.getOrderAggregationPage(page, dto);
        return AjaxResult.success(aggregation);
    }
    @GetMapping("/productionCost/top/product")
    @ApiOperation("消耗排行榜-产品物料Top10")
    public AjaxResult getProductTop10(ProductionCostAccountDto dto) {
        List<ProductionCostAggregationVo> list = productionCostService.getProductTop(dto);
        return AjaxResult.success(list);
    }
    @GetMapping("/productionCost/top/order")
    @ApiOperation("成本排行榜-生产订单Top10")
    public AjaxResult getOrderTop10(ProductionCostAccountDto dto) {
        List<ProductionCostAggregationVo> list = productionCostService.getOrderTop(dto);
        return AjaxResult.success(list);
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOrderRouteController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * ç”Ÿäº§è®¢å•绑定的工艺路线表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author deslrey
 * @since 2026-03-30
 */
@RestController
@RequestMapping("/production-order-route")
public class ProductionOrderRouteController {
}
src/main/java/com/ruoyi/production/dto/GroupKeyDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.production.dto;
import com.ruoyi.production.service.impl.ProductionCostServiceImpl;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDateTime;
import java.util.Objects;
/**
 * <br>
 * èšåˆåˆ†ç»„ Key å¯¹è±¡
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30 16:16
 */
@ApiModel(value = "GroupKeyDtoç±»", description = "聚合分组 Key å¯¹è±¡")
public class GroupKeyDto {
    @ApiModelProperty("日期")
    private final LocalDateTime date;
    @ApiModelProperty("类别/订单名称")
    private final String name;
    @ApiModelProperty("规格型号")
    private final String model;
    @ApiModelProperty("产品类型/强度")
    private final String strength;
    @ApiModelProperty("单位")
    private final String unit;
    public GroupKeyDto(LocalDateTime date, String name, String model, String strength, String unit) {
        this.date = date;
        this.name = name;
        this.model = model;
        this.strength = strength;
        this.unit = unit;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GroupKeyDto groupKey = (GroupKeyDto) o;
        return Objects.equals(date, groupKey.date) &&
                Objects.equals(name, groupKey.name) &&
                Objects.equals(model, groupKey.model) &&
                Objects.equals(strength, groupKey.strength) &&
                Objects.equals(unit, groupKey.unit);
    }
    @Override
    public int hashCode() {
        return Objects.hash(date, name, model, strength, unit);
    }
}
src/main/java/com/ruoyi/production/dto/ProductionCostAccountDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.production.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * <br>
 * æˆæœ¬æ ¸ç®—Dto
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30
 */
@Data
@ApiModel(value = "ProductionCostAccountDtoç±»", description = "成本核算Dto")
public class ProductionCostAccountDto {
    @ApiModelProperty("起始时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDate startDate;
    @ApiModelProperty("截止日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDate endDate;
    @ApiModelProperty("产品类别字典")
    private Long dictCode;
    @ApiModelProperty("生产订单Id")
    private Long productOrderId;
    @ApiModelProperty("分组类型(1: æŒ‰äº§å“ç±»åˆ«æ±‡æ€», 2: æŒ‰ç”Ÿäº§è®¢å•汇总)")
    private Integer groupType;
}
src/main/java/com/ruoyi/production/mapper/ProductionCostMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.production.dto.ProductionCostAccountDto;
import com.ruoyi.production.vo.ProductionCostAggregationVo;
import com.ruoyi.production.vo.ProductionCostDetailVo;
import com.ruoyi.production.vo.ProductionCostSummaryVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductionCostMapper {
    /**
     * æŸ¥è¯¢ç”Ÿäº§æˆæœ¬æ±‡æ€»
     */
    ProductionCostSummaryVo selectCostSummary(@Param("dto") ProductionCostAccountDto dto);
    /**
     * æŒ‰äº§å“ç±»åˆ«æ±‡æ€»æˆæœ¬
     */
    List<ProductionCostAggregationVo> selectCostAggregationByCategory(@Param("dto") ProductionCostAccountDto dto);
    /**
     * æŒ‰ç”Ÿäº§è®¢å•汇总成本
     */
    List<ProductionCostAggregationVo> selectCostAggregationByOrder(@Param("dto") ProductionCostAccountDto dto);
}
src/main/java/com/ruoyi/production/mapper/ProductionOrderRouteMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.production.mapper;
import com.ruoyi.production.pojo.ProductionOrderRoute;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
 * <p>
 * ç”Ÿäº§è®¢å•绑定的工艺路线表 Mapper æŽ¥å£
 * </p>
 *
 * @author deslrey
 * @since 2026-03-30
 */
public interface ProductionOrderRouteMapper extends BaseMapper<ProductionOrderRoute> {
}
src/main/java/com/ruoyi/production/service/IProductionOrderRouteService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.production.service;
import com.ruoyi.production.pojo.ProductionOrderRoute;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * ç”Ÿäº§è®¢å•绑定的工艺路线表 æœåŠ¡ç±»
 * </p>
 *
 * @author deslrey
 * @since 2026-03-30
 */
public interface IProductionOrderRouteService extends IService<ProductionOrderRoute> {
}
src/main/java/com/ruoyi/production/service/ProductionCostService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.dto.ProductionCostAccountDto;
import com.ruoyi.production.vo.ProductionCostAggregationVo;
import com.ruoyi.production.vo.ProductionCostDetailVo;
import com.ruoyi.production.vo.ProductionCostSummaryVo;
import java.util.List;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬æ ¸ç®—服务接口
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30 11:20
 */
public interface ProductionCostService {
    /**
     * èŽ·å–æ±‡æ€»å¡ç‰‡æ•°æ®
     */
    ProductionCostSummaryVo getCostSummary(ProductionCostAccountDto dto);
    /**
     * æŒ‰äº§å“ç±»åˆ«åˆ†é¡µèŽ·å–èšåˆæ±‡æ€»æ•°æ®
     */
    IPage<ProductionCostAggregationVo> getProductAggregationPage(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto);
    /**
     * æŒ‰è®¢å•分页获取聚合汇总数据
     */
    IPage<ProductionCostAggregationVo> getOrderAggregationPage(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto);
    /**
     * èŽ·å–æ¶ˆè€—æœ€é«˜çš„å‰10名产品物料
     */
    List<ProductionCostAggregationVo> getProductTop(ProductionCostAccountDto dto);
    /**
     * èŽ·å–æˆæœ¬æœ€é«˜çš„å‰10个生产订单
     */
    List<ProductionCostAggregationVo> getOrderTop(ProductionCostAccountDto dto);
}
src/main/java/com/ruoyi/production/service/impl/ProductionCostServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.dto.GroupKeyDto;
import com.ruoyi.production.dto.ProductionCostAccountDto;
import com.ruoyi.production.mapper.ProductionCostMapper;
import com.ruoyi.production.service.ProductionCostService;
import com.ruoyi.production.utils.UnitUtils;
import com.ruoyi.production.vo.ProductionCostAggregationVo;
import com.ruoyi.production.vo.ProductionCostSummaryVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬æ ¸ç®—服务接口实现类
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30 11:21
 */
@Slf4j
@Service
public class ProductionCostServiceImpl implements ProductionCostService {
    @Autowired
    private ProductionCostMapper productionCostMapper;
    @Override
    public ProductionCostSummaryVo getCostSummary(ProductionCostAccountDto dto) {
        if (dto.getEndDate() != null) {
            dto.setEndDate(dto.getEndDate().plusDays(1));
        }
        return productionCostMapper.selectCostSummary(dto);
    }
    @Override
    public IPage<ProductionCostAggregationVo> getProductAggregationPage(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto) {
        dto.setGroupType(1); // æŒ‰äº§å“æ±‡æ€»
        List<ProductionCostAggregationVo> fullList = getCostAggregationList(dto);
        return getMemoryPage(page, fullList);
    }
    @Override
    public IPage<ProductionCostAggregationVo> getOrderAggregationPage(Page<ProductionCostAggregationVo> page, ProductionCostAccountDto dto) {
        dto.setGroupType(2); // æŒ‰è®¢å•汇总
        List<ProductionCostAggregationVo> fullList = getCostAggregationList(dto);
        return getMemoryPage(page, fullList);
    }
    @Override
    public List<ProductionCostAggregationVo> getProductTop(ProductionCostAccountDto dto) {
        dto.setGroupType(1);
        List<ProductionCostAggregationVo> fullList = getCostAggregationList(dto);
        if (fullList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<String, ProductionCostAggregationVo> topMap = new HashMap<>();
        for (ProductionCostAggregationVo vo : fullList) {
            String name = vo.getName();
            if (topMap.containsKey(name)) {
                ProductionCostAggregationVo existing = topMap.get(name);
                existing.setTotalCost(existing.getTotalCost().add(vo.getTotalCost()));
                existing.setQuantity(existing.getQuantity().add(vo.getQuantity()));
            } else {
                ProductionCostAggregationVo clone = new ProductionCostAggregationVo();
                clone.setName(name);
                clone.setTotalCost(vo.getTotalCost());
                clone.setQuantity(vo.getQuantity());
                clone.setUnit(vo.getUnit());
                topMap.put(name, clone);
            }
        }
        return topMap.values().stream()
                .sorted(Comparator.comparing(ProductionCostAggregationVo::getTotalCost).reversed())
                .limit(10)
                .collect(Collectors.toList());
    }
    @Override
    public List<ProductionCostAggregationVo> getOrderTop(ProductionCostAccountDto dto) {
        dto.setGroupType(2);
        List<ProductionCostAggregationVo> fullList = getCostAggregationList(dto);
        if (fullList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<String, ProductionCostAggregationVo> topMap = new HashMap<>();
        for (ProductionCostAggregationVo vo : fullList) {
            String orderNo = vo.getName();
            if (topMap.containsKey(orderNo)) {
                ProductionCostAggregationVo existing = topMap.get(orderNo);
                existing.setTotalCost(existing.getTotalCost().add(vo.getTotalCost()));
            } else {
                ProductionCostAggregationVo clone = new ProductionCostAggregationVo();
                clone.setName(orderNo);
                clone.setTotalCost(vo.getTotalCost());
                clone.setStrength(vo.getStrength());
                topMap.put(orderNo, clone);
            }
        }
        return topMap.values().stream()
                .sorted(Comparator.comparing(ProductionCostAggregationVo::getTotalCost).reversed())
                .limit(10)
                .collect(Collectors.toList());
    }
    /**
     * èŽ·å–å…¨é‡èšåˆæ±‡æ€»æ•°æ®
     */
    private List<ProductionCostAggregationVo> getCostAggregationList(ProductionCostAccountDto dto) {
        if (dto.getEndDate() != null) {
            dto.setEndDate(dto.getEndDate().plusDays(1));
        }
        List<ProductionCostAggregationVo> rawList;
        boolean isOrderAggregation = (dto.getGroupType() != null && dto.getGroupType() == 2);
        if (isOrderAggregation) {
            rawList = productionCostMapper.selectCostAggregationByOrder(dto);
        } else {
            rawList = productionCostMapper.selectCostAggregationByCategory(dto);
        }
        if (rawList == null || rawList.isEmpty()) {
            return rawList != null ? rawList : new ArrayList<>();
        }
        Map<GroupKeyDto, ProductionCostAggregationVo> aggregationMap = new LinkedHashMap<>();
        for (ProductionCostAggregationVo vo : rawList) {
            String originalUnit = vo.getUnit();
            String normalizedUnit = UnitUtils.normalizeUnit(originalUnit);
            BigDecimal convertedQty = UnitUtils.convertValueToTon(vo.getQuantity(), originalUnit);
            // æ ¹æ®æ±‡æ€»æ¨¡å¼è®¾ç½® Key å’Œæ˜¾ç¤ºåˆ—
            GroupKeyDto key;
            if (isOrderAggregation) {
                // æŒ‰è®¢å•汇总:Key = æ—¥æœŸ + è®¢å•号 + åŽŸæ–™å + åŽŸæ–™è§„æ ¼ + å•位
                key = new GroupKeyDto(vo.getDate(), vo.getName(), vo.getModel(), vo.getStrength(), normalizedUnit);
            } else {
                // æŒ‰äº§å“æ±‡æ€»ï¼šKey = æ—¥æœŸ + åŽŸæ–™å + åŽŸæ–™è§„æ ¼ + å•位
                key = new GroupKeyDto(vo.getDate(), vo.getName(), vo.getModel(), null, normalizedUnit);
                vo.setStrength(null);
            }
            if (aggregationMap.containsKey(key)) {
                ProductionCostAggregationVo existing = aggregationMap.get(key);
                existing.setQuantity(existing.getQuantity().add(convertedQty));
                existing.setTotalCost(existing.getTotalCost().add(vo.getTotalCost()));
            } else {
                vo.setUnit(normalizedUnit);
                vo.setQuantity(convertedQty);
                aggregationMap.put(key, vo);
            }
        }
        List<ProductionCostAggregationVo> resultList = new ArrayList<>(aggregationMap.values());
        for (ProductionCostAggregationVo vo : resultList) {
            if (vo.getQuantity() != null) {
                vo.setQuantity(vo.getQuantity().setScale(2, RoundingMode.HALF_UP));
            }
            if (vo.getTotalCost() != null) {
                vo.setTotalCost(vo.getTotalCost().setScale(2, RoundingMode.HALF_UP));
            }
        }
        return resultList;
    }
    private <T> IPage<T> getMemoryPage(Page<T> page, List<T> list) {
        int total = list.size();
        int size = (int) page.getSize();
        int current = (int) page.getCurrent();
        int fromIndex = (current - 1) * size;
        int toIndex = Math.min(fromIndex + size, total);
        List<T> subList = (fromIndex < total && fromIndex >= 0) ? list.subList(fromIndex, toIndex) : new ArrayList<>();
        page.setTotal(total);
        page.setRecords(subList);
        return page;
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderAppendixServiceImpl.java
@@ -57,6 +57,9 @@
    @Resource
    private ProductOrderService productOrderService;
    @Resource
    private IProductionOrderRouteService productionOrderRouteService;
    @Override
    public Long populateBlocks(Long orderId, ProductionPlanDto productionPlanDto) {
        if (productionPlanDto == null) {
@@ -70,8 +73,12 @@
            log.info("下发产品【{}】未查询出工艺路线", productionPlanDto.getProductName());
            return null;
        }
        migration(orderId, processRoute);
        return processRoute.getId();
        //  åˆ›å»ºå·¥è‰ºè·¯çº¿
        ProductionOrderRoute productionOrderRoute = createOrderRouteSnapshot(orderId, processRoute);
        migration(orderId, processRoute, productionOrderRoute.getId());
        return productionOrderRoute.getId();
    }
    @Override
@@ -85,8 +92,12 @@
            log.info("下发产品【{}】未查询出工艺路线", productionPlanDto.getProductName());
            return null;
        }
        migration(orderId, processRoute);
        return processRoute.getId();
        //  åˆ›å»ºå·¥è‰ºè·¯çº¿
        ProductionOrderRoute productionOrderRoute = createOrderRouteSnapshot(orderId, processRoute);
        migration(orderId, processRoute, productionOrderRoute.getId());
        return productionOrderRoute.getId();
    }
    @Override
@@ -101,14 +112,27 @@
        if (processRoute == null) {
            throw new ServiceException("该工艺路线不存在,绑定失败");
        }
        migration(productOrder.getId(), processRoute);
        //  å›žå†™å·¥è‰ºè·¯çº¿id到生产订单
        //  åˆ›å»ºå·¥è‰ºè·¯çº¿
        ProductionOrderRoute productionOrderRoute = createOrderRouteSnapshot(productOrder.getId(), processRoute);
        migration(productOrder.getId(), processRoute, productionOrderRoute.getId());
        //  å›žå†™æ–°çš„工艺路线ID到生产订单
        productOrder.setRouteId(productionOrderRoute.getId());
        productOrderService.updateById(productOrder);
    }
    private ProductionOrderRoute createOrderRouteSnapshot(Long orderId, ProcessRoute processRoute) {
        ProductionOrderRoute snapshot = new ProductionOrderRoute();
        BeanUtils.copyProperties(processRoute, snapshot, "id", "createTime", "updateTime", "tenantId");
        snapshot.setOrderId(orderId);
        snapshot.setProcessRouteId(processRoute.getId());
        productionOrderRouteService.save(snapshot);
        return snapshot;
    }
    @Override
    public void deleteData(Long orderId, Long processRouteId) {
    public void deleteData(Long orderId, Long snapshotRouteId) {
        //  åˆ é™¤å·¥è‰ºè·¯çº¿å·¥åºå‚数附表
        productionOrderRouteItemParamService.remove(new LambdaQueryWrapper<ProductionOrderRouteItemParam>()
                .eq(ProductionOrderRouteItemParam::getOrderId, orderId));
@@ -116,25 +140,28 @@
        //  åˆ é™¤å·¥è‰ºè·¯çº¿å­é›†é™„表
        productionOrderRouteItemService.remove(new LambdaQueryWrapper<ProductionOrderRouteItem>()
                .eq(ProductionOrderRouteItem::getOrderId, orderId)
                .eq(ProductionOrderRouteItem::getRouteId, processRouteId));
                .eq(ProductionOrderRouteItem::getRouteId, snapshotRouteId));
        //  åˆ é™¤å·¥è‰ºè·¯çº¿
        productionOrderRouteService.removeById(snapshotRouteId);
        //  åˆ é™¤BOM子集附表
        ProcessRoute processRoute = processRouteService.getById(processRouteId);
        if (processRoute != null && processRoute.getBomId() != null) {
        ProductionOrderRoute snapshot = productionOrderRouteService.getById(snapshotRouteId);
        if (snapshot != null && snapshot.getBomId() != null) {
            productionOrderStructureService.remove(new LambdaQueryWrapper<ProductionOrderStructure>()
                    .eq(ProductionOrderStructure::getOrderId, orderId)
                    .eq(ProductionOrderStructure::getBomId, processRoute.getBomId()));
                    .eq(ProductionOrderStructure::getBomId, snapshot.getBomId().longValue()));
        }
    }
    /**
     * æ ¹æ®å·¥è‰ºè·¯çº¿è¿ç§»é™„表数据
     */
    private void migration(Long orderId, ProcessRoute processRoute) {
    private void migration(Long orderId, ProcessRoute processRoute, Long newRouteId) {
        //  è¿ç§»å·¥è‰ºè·¯çº¿å­é›†è¡¨æ•°æ®ï¼Œè¿”回旧id->新instance映射
        List<ProcessRouteItem> processRouteItemList = processRouteItemService.list(
                new LambdaQueryWrapper<ProcessRouteItem>().eq(ProcessRouteItem::getRouteId, processRoute.getId()));
        Map<Long, ProductionOrderRouteItem> routeItemOldIdMap = migrationProcessRouteItem(orderId, processRouteItemList);
        Map<Long, ProductionOrderRouteItem> routeItemOldIdMap = migrationProcessRouteItem(orderId, newRouteId, processRouteItemList);
        //  è¿ç§»å·¥è‰ºè·¯çº¿å†…绑定的工序参数
        if (processRouteItemList != null && !processRouteItemList.isEmpty()) {
@@ -156,7 +183,7 @@
        }
    }
    private Map<Long, ProductionOrderRouteItem> migrationProcessRouteItem(Long orderId, List<ProcessRouteItem> list) {
    private Map<Long, ProductionOrderRouteItem> migrationProcessRouteItem(Long orderId, Long newRouteId, List<ProcessRouteItem> list) {
        Map<Long, ProductionOrderRouteItem> oldIdMap = new HashMap<>();
        if (list == null || list.isEmpty()) {
            return oldIdMap;
@@ -166,6 +193,7 @@
            BeanUtils.copyProperties(item, instance, "id");
            instance.setIsQuality(item.getIsQuality() != null && item.getIsQuality() ? 1 : 0);
            instance.setOrderId(orderId);
            instance.setRouteId(newRouteId);
            if (item.getId() != null) {
                oldIdMap.put(item.getId(), instance);
            }
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRouteServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.service.impl;
import com.ruoyi.production.pojo.ProductionOrderRoute;
import com.ruoyi.production.mapper.ProductionOrderRouteMapper;
import com.ruoyi.production.service.IProductionOrderRouteService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ç”Ÿäº§è®¢å•绑定的工艺路线表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author deslrey
 * @since 2026-03-30
 */
@Service
public class ProductionOrderRouteServiceImpl extends ServiceImpl<ProductionOrderRouteMapper, ProductionOrderRoute> implements IProductionOrderRouteService {
}
src/main/java/com/ruoyi/production/utils/UnitUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.production.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
 * <br>
 * å•位转换工具类
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30 15:07
 */
public class UnitUtils {
    private static final Set<String> WEIGHT_UNITS = new HashSet<>(Arrays.asList(
            "千克", "公斤", "kg", "KG", "kG",
            "克", "g", "G",
            "吨", "t", "T"
    ));
    private static final Set<String> KG_UNITS = new HashSet<>(Arrays.asList("千克", "公斤", "kg", "KG", "kG"));
    private static final Set<String> G_UNITS = new HashSet<>(Arrays.asList("克", "g", "G"));
    private static final Set<String> TON_UNITS = new HashSet<>(Arrays.asList("吨", "t", "T"));
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºé‡é‡å•位
     */
    public static boolean isWeightUnit(String unit) {
        return unit != null && WEIGHT_UNITS.contains(unit.trim());
    }
    /**
     * å¦‚果是重量单位,统一返回“吨”,否则返回原单位
     */
    public static String normalizeUnit(String unit) {
        if (isWeightUnit(unit)) {
            return "吨";
        }
        return unit != null ? unit.trim() : "未知单位";
    }
    /**
     * å¦‚果是重量单位,折算为“吨”,否则返回原值
     */
    public static BigDecimal convertValueToTon(BigDecimal value, String unit) {
        if (value == null || unit == null) {
            return value != null ? value : BigDecimal.ZERO;
        }
        String trimmedUnit = unit.trim();
        if (KG_UNITS.contains(trimmedUnit)) {
            return value.divide(new BigDecimal("1000"), 6, RoundingMode.HALF_UP);
        } else if (G_UNITS.contains(trimmedUnit)) {
            return value.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
        } else if (TON_UNITS.contains(trimmedUnit)) {
            return value;
        }
        return value;
    }
}
src/main/java/com/ruoyi/production/vo/ProductionCostAggregationVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package com.ruoyi.production.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬èšåˆæ±‡æ€»VO
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30
 */
@Data
@ApiModel("生产成本聚合汇总VO")
public class ProductionCostAggregationVo {
    @ApiModelProperty("日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime date;
    @ApiModelProperty("类别/订单名称")
    private String name;
    @ApiModelProperty("规格型号")
    private String model;
    @ApiModelProperty("产品类型/强度")
    private String strength;
    @ApiModelProperty("用量")
    private BigDecimal quantity;
    @ApiModelProperty("单位")
    private String unit;
    @ApiModelProperty("总成本(元)")
    private BigDecimal totalCost;
}
src/main/java/com/ruoyi/production/vo/ProductionCostDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.ruoyi.production.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬åˆ†é¡µæ˜Žç»†VO
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30
 */
@Data
@ApiModel("生产成本分页明细VO")
public class ProductionCostDetailVo {
    @ApiModelProperty("日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime date;
    @ApiModelProperty("产品名称")
    private String productName;
    @ApiModelProperty("产品类型")
    private String categoryLabel;
    @ApiModelProperty("规格型号")
    private String model;
    @ApiModelProperty("产品类型/强度")
    private String strength;
    @ApiModelProperty("生产订单")
    private String orderNo;
    @ApiModelProperty("用量")
    private BigDecimal quantity;
    @ApiModelProperty("单位")
    private String unit;
    @ApiModelProperty("成本(元)")
    private BigDecimal cost;
}
src/main/java/com/ruoyi/production/vo/ProductionCostSummaryVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.production.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
 * <br>
 * ç”Ÿäº§æˆæœ¬ç»Ÿè®¡æ±‡æ€»VO
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/30
 */
@Data
@ApiModel("生产成本统计汇总VO")
public class ProductionCostSummaryVo {
    @ApiModelProperty("总生产成本")
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private BigDecimal totalCost;
    @ApiModelProperty("每订单平均成本")
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private BigDecimal averageOrderCost;
    @ApiModelProperty("订单数量")
    private Long orderCount;
}
src/main/resources/mapper/production/ProductOrderMapper.xml
@@ -52,7 +52,7 @@
        pms.material_code AS materialCode,
        pms.id AS productId
        FROM product_order po
        LEFT JOIN process_route pr ON po.route_id = pr.id
        LEFT JOIN production_order_route pr ON po.route_id = pr.id
        LEFT JOIN product_bom pb ON pr.bom_id = pb.id
        LEFT JOIN product_material_sku pms ON pms.id = po.product_material_sku_id
        LEFT JOIN product_material pm ON pm.id = pms.product_id
src/main/resources/mapper/production/ProductionCostMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.production.mapper.ProductionCostMapper">
    <sql id="baseCostQuery">
        SELECT
        ppi.quantity,
        pos.unit_price,
        (ppi.quantity * IFNULL(pos.unit_price, 0)) as calculated_cost,
        ppi.unit as unit,
        ppm.product_order_id,
        ppm.reporting_time as log_date,
        po.nps_no as order_no,
        pm_mat.product_name as material_name,
        pms_mat.model as material_model,
        po.strength as strength,
        sdd_type.dict_label as category_label
        FROM production_product_input ppi
        JOIN production_product_main ppm ON ppi.product_main_id = ppm.id
        JOIN product_order po ON ppm.product_order_id = po.id
        LEFT JOIN production_order_route pr ON po.route_id = pr.id
        LEFT JOIN sys_dict_data sdd_type ON pr.dict_code = sdd_type.dict_code
        LEFT JOIN production_order_structure pos ON ppm.product_order_id = pos.order_id
        AND ppi.product_id = pos.product_model_id
        AND ppi.bom_id = pos.bom_id
        LEFT JOIN product_material_sku pms_mat ON ppi.product_id = pms_mat.id
        LEFT JOIN product_material pm_mat ON pms_mat.product_id = pm_mat.id
        <where>
            <if test="dto.startDate != null">
                AND ppm.reporting_time &gt;= #{dto.startDate}
            </if>
            <if test="dto.endDate != null">
                AND ppm.reporting_time &lt; #{dto.endDate}
            </if>
            <if test="dto.dictCode != null">
                AND pr.dict_code = #{dto.dictCode}
            </if>
            <if test="dto.productOrderId != null">
                AND ppm.product_order_id = #{dto.productOrderId}
            </if>
        </where>
    </sql>
    <select id="selectCostSummary" resultType="com.ruoyi.production.vo.ProductionCostSummaryVo">
        SELECT
        ROUND(IFNULL(SUM(calculated_cost), 0), 2) as totalCost,
        COUNT(DISTINCT product_order_id) as orderCount,
        CASE
        WHEN COUNT(DISTINCT product_order_id) > 0
        THEN ROUND(SUM(calculated_cost) / COUNT(DISTINCT product_order_id), 2)
        ELSE 0
        END as averageOrderCost
        FROM (
        <include refid="baseCostQuery"/>
        ) t
    </select>
    <select id="selectCostAggregationByCategory" resultType="com.ruoyi.production.vo.ProductionCostAggregationVo">
        SELECT
        log_date as date,
        IFNULL(material_name, '未知产品') as name,
        material_model as model,
        unit,
        ROUND(IFNULL(SUM(quantity), 0), 6) as quantity,
        ROUND(IFNULL(SUM(calculated_cost), 0), 2) as totalCost
        FROM (
        <include refid="baseCostQuery"/>
        ) t
        GROUP BY log_date, material_name, material_model, unit
    </select>
    <select id="selectCostAggregationByOrder" resultType="com.ruoyi.production.vo.ProductionCostAggregationVo">
        SELECT
        log_date as date,
        IFNULL(order_no, '未知单号') as name,
        material_name as model,
        category_label as strength,
        unit,
        ROUND(IFNULL(SUM(quantity), 0), 6) as quantity,
        ROUND(IFNULL(SUM(calculated_cost), 0), 2) as totalCost
        FROM (
        <include refid="baseCostQuery"/>
        ) t
        GROUP BY log_date, order_no, material_name, category_label, unit
    </select>
</mapper>
src/main/resources/mapper/production/ProductionOrderRouteMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.production.mapper.ProductionOrderRouteMapper">
    <resultMap id="BaseResultMap" type="com.ruoyi.production.pojo.ProductionOrderRoute">
        <id column="id" property="id"/>
        <result column="order_id" property="orderId"/>
        <result column="process_route_id" property="processRouteId"/>
        <result column="product_model_id" property="productModelId"/>
        <result column="process_route_code" property="processRouteCode"/>
        <result column="bom_id" property="bomId"/>
        <result column="dict_code" property="dictCode"/>
        <result column="description" property="description"/>
        <result column="tenant_id" property="tenantId"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
        <result column="remark" property="remark"/>
    </resultMap>
</mapper>