liu
4 天以前 a5f66f55b2c176e8504b8a077e00acfe2dfb4b00
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -7,16 +7,21 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.service.ApprovalTemplateService;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
@@ -26,6 +31,8 @@
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.*;
import com.ruoyi.quality.pojo.*;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
@@ -42,6 +49,7 @@
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -55,6 +63,16 @@
@AllArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ProductionProductMainServiceImpl extends ServiceImpl<ProductionProductMainMapper, ProductionProductMain> implements ProductionProductMainService {
    /**
     * 生产报工大类:只区分小提琴/大提琴/贝斯,不再区分零件。
     * key为入库规范值,value为产品名称匹配别名(现场数据贝斯写作“贝司”)。
     */
    private static final Map<String, List<String>> MAJOR_CATEGORY_PATTERNS = new LinkedHashMap<>() {{
        put("小提琴", List.of("小提琴"));
        put("大提琴", List.of("大提琴"));
        put("贝斯", List.of("贝斯", "贝司"));
    }};
    private final ProductionProductMainMapper productionProductMainMapper;
    private final SysUserMapper userMapper;
@@ -70,13 +88,14 @@
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final ApprovalTemplateService approvalTemplateService;
    private final ApprovalInstanceService approvalInstanceService;
    private final StockUtils stockUtils;
    private final StockInRecordService stockInRecordService;
    private final StockInventoryService stockInventoryService;
    @Override
@@ -268,45 +287,58 @@
            throw new ServiceException("产品规格不存在");
        }
        // 报工完成时对计数的零件做区分:合格/轻微返工/严重返工/报废四类数量。
        BigDecimal qualifiedQty = defaultDecimal(dto.getQuantity());
        BigDecimal reworkMinorQty = defaultDecimal(dto.getReworkMinorQty());
        BigDecimal reworkMajorQty = defaultDecimal(dto.getReworkMajorQty());
        BigDecimal scrapQtyInput = defaultDecimal(dto.getScrapQty());
        if (qualifiedQty.compareTo(BigDecimal.ZERO) < 0
                || reworkMinorQty.compareTo(BigDecimal.ZERO) < 0
                || reworkMajorQty.compareTo(BigDecimal.ZERO) < 0
                || scrapQtyInput.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("报工数量不能为负数");
        }
        if (qualifiedQty.add(reworkMinorQty).add(reworkMajorQty).add(scrapQtyInput).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("请按合格/轻微返工/严重返工/报废分类填写零件数量");
        }
        ProductionProductMain productionProductMain = new ProductionProductMain();
        productionProductMain.setProductNo(generateProductNo());
        productionProductMain.setMajorCategory(resolveMajorCategory(productModel));
        productionProductMain.setUserId(user == null ? dto.getUserId() : user.getUserId());
        productionProductMain.setUserName(user == null ? dto.getUserName() : user.getNickName());
        productionProductMain.setProductionOperationTaskId(taskId);
        productionProductMain.setStatus(0);
        productionProductMain.setReportType(dto.getReportType() != null
                ? dto.getReportType()
                : deriveReportType(qualifiedQty, reworkMinorQty, reworkMajorQty, scrapQtyInput));
        productionProductMain.setAuditStatus(dto.getAuditStatus() == null ? 0 : dto.getAuditStatus());
        productionProductMain.setAuditRemark(dto.getAuditRemark());
        productionProductMain.setWorkHour(dto.getWorkHour());
        productionProductMainMapper.insert(productionProductMain);
        syncOperationParamInputValue(dto, routingOperation.getId(), productionProductMain.getId());
        createReportApprovalInstance(productionProductMain);
        List<ProductStructureDto> productStructureDtos = resolveInputStructures(
                productionOrder.getId(), routingOperation, productModel.getId());
       // 如果没有bom子节点了,那么投入就是他本身
        if (productStructureDtos.isEmpty()) {
            ProductStructureDto fallbackInput = new ProductStructureDto();
            fallbackInput.setProductModelId(productModel.getId());
            fallbackInput.setUnitQuantity(BigDecimal.ONE);
            productStructureDtos.add(fallbackInput);
        }
        for (ProductStructureDto item : productStructureDtos) {
            // 当前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
            ProductionProductInput productionProductInput = new ProductionProductInput();
            productionProductInput.setProductionProductMainId(productionProductMain.getId());
            productionProductInput.setProductMainId(productionProductMain.getId());
            productionProductInput.setProductModelId(item.getProductModelId());
            productionProductInput.setInputQuantity(item.getUnitQuantity().multiply(defaultDecimal(dto.getQuantity())));
            productionProductInput.setQuantity(productionProductInput.getInputQuantity());
            productionProductInputMapper.insert(productionProductInput);
        }
        // 需求变更:生产报工不再按零件(BOM结构)拆分投入,投入即本次报工的产品本身,按大类统计。
        ProductionProductInput productionProductInput = new ProductionProductInput();
        productionProductInput.setProductionProductMainId(productionProductMain.getId());
        productionProductInput.setProductModelId(productModel.getId());
        productionProductInput.setInputQuantity(qualifiedQty);
        productionProductInput.setQuantity(qualifiedQty);
        productionProductInputMapper.insert(productionProductInput);
        ProductionProductOutput productionProductOutput = new ProductionProductOutput();
        productionProductOutput.setProductionProductMainId(productionProductMain.getId());
        productionProductOutput.setProductMainId(productionProductMain.getId());
        productionProductOutput.setProductModelId(productModel.getId());
        productionProductOutput.setQuantity(defaultDecimal(dto.getQuantity()));
        productionProductOutput.setScrapQty(defaultDecimal(dto.getScrapQty()));
        productionProductOutput.setReworkMinorQty(reworkMinorQty);
        productionProductOutput.setReworkMajorQty(reworkMajorQty);
        productionProductOutputMapper.insert(productionProductOutput);
        BigDecimal reportQty = defaultDecimal(productionProductOutput.getQuantity());
        BigDecimal scrapQty = defaultDecimal(productionProductOutput.getScrapQty());
        BigDecimal productQty = reportQty;
        String qualifiedBatchNo = null;
        List<ProductionOrderRoutingOperation> routingOperationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
@@ -345,13 +377,19 @@
                                qualityInspectParamMapper.insert(param);
                            });
                }
            } else {
            } else if(isLastOperation){
                //(最后一道工序才入库)
                StockInventoryDto stockInventoryDto = new StockInventoryDto();
                stockInventoryDto.setRecordId(productionProductMain.getId());
                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode()));
                stockInventoryDto.setQualitity(productQty);
                stockInventoryDto.setProductModelId(productModel.getId());
                stockInventoryService.addStockInRecordOnly(stockInventoryDto);
                qualifiedBatchNo = resolveLatestStockInBatchNo(
                        productionProductMain.getId(),
                        StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(),
                        productModel.getId(),
                        "0");
            }
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).add(productQty));
@@ -370,8 +408,10 @@
            if (ObjectUtils.isNull(productionOrder.getStartTime())) {
                productionOrder.setStartTime(LocalDateTime.now());
            }
            // 订单状态由最后一道工序的合格产出推动,避免中间工序提前完工。
            productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
            // 仅待开始状态才推进到进行中,避免非末道工序报工时覆盖已完成状态。
            if (ProductOrderStatusEnum.WAIT.getCode().equals(productionOrder.getStatus())) {
                productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
            }
            if (isLastOperation) {
                productionOrder.setCompleteQuantity(defaultDecimal(productionOrder.getCompleteQuantity()).add(productQty));
                if (productionOrder.getQuantity() != null
@@ -393,12 +433,246 @@
            productionAccount.setSchedulingUserId(user == null ? null : user.getUserId());
            productionAccount.setSchedulingUserName(user == null ? dto.getUserName() : user.getNickName());
            productionAccount.setFinishedNum(productQty);
            productionAccount.setWorkHours(workHours);
            productionAccount.setWorkHours(technologyOperation != null ? technologyOperation.getSalaryQuota() : null);
            productionAccount.setTechnologyOperationName(technologyOperation == null ? null : technologyOperation.getName());
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
        }
        if (scrapQty.compareTo(BigDecimal.ZERO) > 0) {
            stockUtils.addUnStockWithBatchNo(
                    productModel.getId(),
                    scrapQty,
                    StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(),
                    productionProductMain.getId(),
                    qualifiedBatchNo);
        }
        // 区分出的返工零件生成不合格品待处理单,进入不合格品管理的报工处理闭环。
        createReworkUnqualifiedRecords(productionProductMain, productModel, reworkMinorQty, reworkMajorQty);
        return true;
    }
    private Integer deriveReportType(BigDecimal qualifiedQty, BigDecimal reworkMinorQty,
                                     BigDecimal reworkMajorQty, BigDecimal scrapQty) {
        if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
            return 0;
        }
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) > 0) {
            return 1;
        }
        if (reworkMajorQty.compareTo(BigDecimal.ZERO) > 0) {
            return 2;
        }
        if (scrapQty.compareTo(BigDecimal.ZERO) > 0) {
            return 3;
        }
        return 0;
    }
    private void createReworkUnqualifiedRecords(ProductionProductMain productionProductMain,
                                                ProductModel productModel,
                                                BigDecimal reworkMinorQty,
                                                BigDecimal reworkMajorQty) {
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) <= 0 && reworkMajorQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        Product product = productMapper.selectById(productModel.getProductId());
        if (product == null) {
            throw new ServiceException("产品不存在,无法生成报工返工待处理单");
        }
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) > 0) {
            insertReworkUnqualified(productionProductMain, product, productModel, reworkMinorQty, 0);
        }
        if (reworkMajorQty.compareTo(BigDecimal.ZERO) > 0) {
            insertReworkUnqualified(productionProductMain, product, productModel, reworkMajorQty, 1);
        }
    }
    private void insertReworkUnqualified(ProductionProductMain productionProductMain,
                                         Product product,
                                         ProductModel productModel,
                                         BigDecimal quantity,
                                         int dealType) {
        QualityUnqualified qualityUnqualified = new QualityUnqualified();
        qualityUnqualified.setInspectType(1);
        qualityUnqualified.setInspectState(0);
        qualityUnqualified.setCheckTime(new Date());
        qualityUnqualified.setCheckName(productionProductMain.getUserName());
        qualityUnqualified.setProductId(product.getId());
        qualityUnqualified.setProductName(product.getProductName());
        qualityUnqualified.setModel(productModel.getModel());
        qualityUnqualified.setUnit(productModel.getUnit());
        qualityUnqualified.setQuantity(quantity);
        qualityUnqualified.setProductModelId(productModel.getId());
        qualityUnqualified.setProductMainId(productionProductMain.getId());
        qualityUnqualified.setDefectivePhenomena("生产报工【" + productionProductMain.getProductNo()
                + "】计数区分-" + (dealType == 0 ? "轻微返工" : "严重返工"));
        qualityUnqualified.setDealType(dealType);
        qualityUnqualified.setDealResult("报工");
        qualityUnqualifiedMapper.insert(qualityUnqualified);
    }
    private void createReportApprovalInstance(ProductionProductMain productionProductMain) {
        if (productionProductMain == null || productionProductMain.getId() == null) {
            return;
        }
        ApprovalTemplateVo templateVo = approvalTemplateService.getEnabledTemplateByBusinessType(
                TypeEnums.PRODUCTION_REPORT_APPROVAL.getCode()
        );
        if (templateVo == null) {
            throw new ServiceException("请先配置报工审批模板");
        }
        ApprovalInstanceDto approvalInstanceDto = new ApprovalInstanceDto();
        approvalInstanceDto.setTemplateId(templateVo.getId());
        approvalInstanceDto.setTemplateName(templateVo.getTemplateName());
        approvalInstanceDto.setBusinessId(productionProductMain.getId());
        approvalInstanceDto.setBusinessType(TypeEnums.PRODUCTION_REPORT_APPROVAL.getCode());
        approvalInstanceDto.setTitle(productionProductMain.getProductNo());
        approvalInstanceDto.setApplicantId(productionProductMain.getUserId());
        approvalInstanceDto.setApplicantName(productionProductMain.getUserName());
        approvalInstanceDto.setApplyTime(LocalDateTime.now());
        approvalInstanceDto.setStatus("PENDING");
        approvalInstanceDto.setFormConfig(JSON.toJSONString(buildReportApprovalFormConfig(productionProductMain)));
        approvalInstanceService.add(approvalInstanceDto);
    }
    private Map<String, Object> buildReportApprovalFormConfig(ProductionProductMain productionProductMain) {
        Map<String, Object> formConfig = new LinkedHashMap<>();
        formConfig.put("summaryPlaceholder", "报工审批");
        formConfig.put("approvalType", "production_report");
        ProductionOperationTask task = productionOperationTaskMapper.selectById(productionProductMain.getProductionOperationTaskId());
        ProductionProductOutput output = productionProductOutputMapper.selectOne(
                Wrappers.<ProductionProductOutput>lambdaQuery()
                        .eq(ProductionProductOutput::getProductionProductMainId, productionProductMain.getId())
                        .last("limit 1"));
        BigDecimal schedulingNum = task == null ? BigDecimal.ZERO : defaultDecimal(task.getPlanQuantity());
        BigDecimal finishedNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getQuantity());
        BigDecimal scrapQty = output == null ? BigDecimal.ZERO : defaultDecimal(output.getScrapQty());
        BigDecimal reworkMinorNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getReworkMinorQty());
        BigDecimal reworkMajorNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getReworkMajorQty());
        BigDecimal pendingNum = schedulingNum.subtract(finishedNum);
        if (pendingNum.compareTo(BigDecimal.ZERO) < 0) {
            pendingNum = BigDecimal.ZERO;
        }
        List<Map<String, Object>> fields = new ArrayList<>();
        fields.add(buildReportField("productNo", "报工单号", "text", productionProductMain.getProductNo()));
        fields.add(buildReportField("userName", "报工人员", "text", productionProductMain.getUserName()));
        fields.add(buildReportField("teamInfo", "班组信息", "text", productionProductMain.getUserName()));
        fields.add(buildReportField("workOrderNo", "工单编号", "text", resolveWorkOrderNo(productionProductMain.getProductionOperationTaskId())));
        fields.add(buildReportField("schedulingNum", "排产数量", "number", schedulingNum));
        fields.add(buildReportField("pendingNum", "待生产数量", "number", pendingNum));
        fields.add(buildReportField("finishedNum", "合格数量", "number", finishedNum));
        fields.add(buildReportField("reworkMinorQty", "轻微返工数量", "number", reworkMinorNum));
        fields.add(buildReportField("reworkMajorQty", "严重返工数量", "number", reworkMajorNum));
        fields.add(buildReportField("scrapQty", "报废数量", "number", scrapQty));
        fields.add(buildReportField("reportType", "报工结果", "select", resolveReportTypeLabel(productionProductMain.getReportType())));
        fields.add(buildReportField("workHour", "工时", "number", productionProductMain.getWorkHour()));
        fields.add(buildReportField("auditStatus", "审核状态", "select", resolveAuditStatusLabel(productionProductMain.getAuditStatus())));
        fields.add(buildReportField("auditRemark", "审核意见", "textarea", productionProductMain.getAuditRemark()));
        formConfig.put("fields", fields);
        Map<String, Object> formPayload = new LinkedHashMap<>();
        formPayload.put("productNo", productionProductMain.getProductNo());
        formPayload.put("userName", productionProductMain.getUserName());
        formPayload.put("teamInfo", productionProductMain.getUserName());
        formPayload.put("workOrderNo", resolveWorkOrderNo(productionProductMain.getProductionOperationTaskId()));
        formPayload.put("schedulingNum", schedulingNum);
        formPayload.put("pendingNum", pendingNum);
        formPayload.put("finishedNum", finishedNum);
        formPayload.put("scrapQty", scrapQty);
        formPayload.put("reworkMinorQty", reworkMinorNum);
        formPayload.put("reworkMajorQty", reworkMajorNum);
        formPayload.put("reportType", resolveReportTypeLabel(productionProductMain.getReportType()));
        formPayload.put("workHour", productionProductMain.getWorkHour());
        formPayload.put("auditStatus", resolveAuditStatusLabel(productionProductMain.getAuditStatus()));
        formPayload.put("auditRemark", productionProductMain.getAuditRemark());
        formConfig.put("formPayload", formPayload);
        return formConfig;
    }
    private Map<String, Object> buildReportField(String key, String label, String type, Object defaultValue) {
        Map<String, Object> field = new LinkedHashMap<>();
        field.put("key", key);
        field.put("label", label);
        field.put("type", type);
        field.put("required", Boolean.FALSE);
        field.put("defaultValue", defaultValue);
        return field;
    }
    private String resolveWorkOrderNo(Long productionOperationTaskId) {
        if (productionOperationTaskId == null) {
            return null;
        }
        ProductionOperationTask task = productionOperationTaskMapper.selectById(productionOperationTaskId);
        if (task != null && task.getWorkOrderNo() != null) {
            return task.getWorkOrderNo();
        }
        if (task == null || task.getProductionOrderRoutingOperationId() == null) {
            return null;
        }
        ProductionOrderRoutingOperation routingOperation = productionOrderRoutingOperationMapper.selectById(task.getProductionOrderRoutingOperationId());
        if (routingOperation == null || routingOperation.getProductionOrderId() == null) {
            return null;
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(routingOperation.getProductionOrderId());
        return productionOrder == null ? null : productionOrder.getNpsNo();
    }
    private String resolveReportTypeLabel(Integer reportType) {
        if (reportType == null) {
            return "—";
        }
        switch (reportType) {
            case 0:
                return "合格";
            case 1:
                return "轻微返工";
            case 2:
                return "严重返工";
            case 3:
                return "报废";
            default:
                return "—";
        }
    }
    private String resolveAuditStatusLabel(Integer auditStatus) {
        if (auditStatus == null) {
            return "待审核";
        }
        switch (auditStatus) {
            case 0:
                return "待审核";
            case 1:
                return "审核通过";
            case 2:
                return "审核不通过";
            default:
                return "—";
        }
    }
    private String resolveLatestStockInBatchNo(Long recordId,
                                               String recordType,
                                               Long productModelId,
                                               String stockType) {
        if (recordId == null || productModelId == null) {
            return null;
        }
        StockInRecord stockInRecord = stockInRecordService.getOne(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordId, recordId)
                        .eq(StockInRecord::getRecordType, recordType)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .eq(StockInRecord::getType, stockType)
                        .orderByDesc(StockInRecord::getId)
                        .last("limit 1"),
                false);
        if (stockInRecord == null) {
            throw new ServiceException("未找到对应的入库申请记录");
        }
        return stockInRecord.getBatchNo();
    }
    private void syncOperationParamInputValue(ProductionProductMainDto dto,
@@ -466,62 +740,46 @@
        return target;
    }
    private List<ProductStructureDto> resolveInputStructures(Long productionOrderId,
                                                             ProductionOrderRoutingOperation routingOperation,
                                                             Long outputProductModelId) {
        if (productionOrderId == null || routingOperation == null || routingOperation.getTechnologyOperationId() == null) {
            return new ArrayList<>();
    /**
     * 需求:生产报工只区分产品大类(小提琴/大提琴/贝斯),不再区分零件。
     * 从产出产品自身名称开始沿产品树向上做关键字匹配,命中即取对应规范大类;未命中返回 null。
     * 产品树顶层为“成品/半成品/原料”等库存分类,不能直接当作大类,因此必须自底向上取第一个命中的层级。
     */
    private String resolveMajorCategory(ProductModel productModel) {
        if (productModel == null || productModel.getProductId() == null) {
            return null;
        }
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderBom::getId)
                        .last("limit 1"));
        if (orderBom == null || orderBom.getId() == null) {
            return new ArrayList<>();
        }
        List<ProductionBomStructure> bomNodeList = productionBomStructureMapper.selectList(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        if (bomNodeList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<Long, ProductionBomStructure> nodeMap = bomNodeList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Long currentOutputModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : outputProductModelId;
        Map<Long, BigDecimal> unitQtyByProductModel = new LinkedHashMap<>();
        for (ProductionBomStructure node : bomNodeList) {
            if (node == null || node.getParentId() == null || node.getProductModelId() == null) {
                continue;
        Product product = productMapper.selectById(productModel.getProductId());
        Set<Long> visited = new LinkedHashSet<>();
        while (product != null && visited.add(product.getId())) {
            String category = matchMajorCategory(product.getProductName());
            if (category != null) {
                return category;
            }
            if (!Objects.equals(node.getTechnologyOperationId(), routingOperation.getTechnologyOperationId())) {
                continue;
            if (product.getParentId() == null || product.getParentId() == 0L) {
                break;
            }
            ProductionBomStructure parent = nodeMap.get(node.getParentId());
            if (parent == null || !Objects.equals(parent.getProductModelId(), currentOutputModelId)) {
                continue;
            }
            unitQtyByProductModel.merge(node.getProductModelId(), defaultDecimal(node.getUnitQuantity()), BigDecimal::add);
            product = productMapper.selectById(product.getParentId());
        }
        return null;
    }
        List<ProductStructureDto> result = new ArrayList<>();
        for (Map.Entry<Long, BigDecimal> entry : unitQtyByProductModel.entrySet()) {
            if (entry.getValue().compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            ProductStructureDto item = new ProductStructureDto();
            item.setProductModelId(entry.getKey());
            item.setUnitQuantity(entry.getValue());
            result.add(item);
    static String matchMajorCategory(String productName) {
        if (productName == null || productName.isEmpty()) {
            return null;
        }
        return result;
        // 琴盒/琴套等配件名称可能带“贝司”等字样(如 SA贝司套),排除后避免误归大类。
        if (productName.contains("琴盒") || productName.contains("琴套") || productName.contains("弓盒")) {
            return null;
        }
        for (Map.Entry<String, List<String>> entry : MAJOR_CATEGORY_PATTERNS.entrySet()) {
            for (String alias : entry.getValue()) {
                if (productName.contains(alias)) {
                    return entry.getKey();
                }
            }
        }
        return null;
    }
    private Boolean removeProductMainByProductionTask(ProductionProductMain productionProductMain) {
@@ -537,6 +795,13 @@
            if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState() == 1) {
                throw new ServiceException("该条报工已经不合格处理了,不允许删除");
            }
        }
        // 报工计数区分直接产生的返工待处理单:已处理的不允许删除报工,未处理的随报工一并删除。
        List<QualityUnqualified> reworkUnqualifieds = qualityUnqualifiedMapper.selectList(
                Wrappers.<QualityUnqualified>lambdaQuery()
                        .eq(QualityUnqualified::getProductMainId, productionProductMain.getId()));
        if (reworkUnqualifieds.stream().anyMatch(q -> Integer.valueOf(1).equals(q.getInspectState()))) {
            throw new ServiceException("该条报工的返工零件已做不合格处理,不允许删除");
        }
        ProductionProductOutput productionProductOutput = productionProductOutputMapper.selectList(
                Wrappers.<ProductionProductOutput>lambdaQuery()
@@ -584,6 +849,8 @@
            }
        }
        qualityUnqualifiedMapper.delete(new LambdaQueryWrapper<QualityUnqualified>()
                .eq(QualityUnqualified::getProductMainId, productionProductMain.getId()));
        qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getProductMainId, productionProductMain.getId())).forEach(q -> {
            qualityInspectParamMapper.delete(new LambdaQueryWrapper<QualityInspectParam>()
@@ -641,6 +908,87 @@
        return dto.getProductionOperationTaskId();
    }
    private boolean isCountingWageReport(ProductionProductMain productionProductMain) {
        return productionProductMain != null
                && productionProductMain.getAuditStatus() != null
                && productionProductMain.getAuditStatus() == 1
                && productionProductMain.getReportType() != null
                && (productionProductMain.getReportType() == 0 || productionProductMain.getReportType() == 1);
    }
    private boolean isExcludedFromWage(ProductionProductMain productionProductMain) {
        return productionProductMain != null
                && productionProductMain.getReportType() != null
                && productionProductMain.getReportType() != 0
                && productionProductMain.getReportType() != 1;
    }
    @Override
    public Boolean auditProductMain(ProductionProductMainDto productionProductMainDto) {
        if (productionProductMainDto == null || productionProductMainDto.getId() == null) {
            throw new ServiceException("参数错误");
        }
        ProductionProductMain productionProductMain = productionProductMainMapper.selectById(productionProductMainDto.getId());
        if (productionProductMain == null) {
            throw new ServiceException("报工不存在");
        }
        Integer auditStatus = productionProductMainDto.getAuditStatus();
        if (auditStatus == null) {
            throw new ServiceException("审核状态不能为空");
        }
        productionProductMain.setAuditStatus(auditStatus);
        productionProductMain.setAuditRemark(productionProductMainDto.getAuditRemark());
        productionProductMainMapper.updateById(productionProductMain);
        productionAccountMapper.delete(new LambdaQueryWrapper<ProductionAccount>()
                .eq(ProductionAccount::getProductionProductMainId, productionProductMain.getId()));
        if (auditStatus == 1 && !isExcludedFromWage(productionProductMain)) {
            rebuildProductionAccount(productionProductMain);
        }
        return true;
    }
    private void rebuildProductionAccount(ProductionProductMain productionProductMain) {
        if (productionProductMain == null || productionProductMain.getId() == null) {
            return;
        }
        ProductionProductOutput output = productionProductOutputMapper.selectOne(
                Wrappers.<ProductionProductOutput>lambdaQuery()
                        .eq(ProductionProductOutput::getProductionProductMainId, productionProductMain.getId())
                        .last("limit 1"));
        if (output == null) {
            return;
        }
        ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectById(productionProductMain.getProductionOperationTaskId());
        if (productionOperationTask == null) {
            return;
        }
        ProductionOrderRoutingOperation routingOperation = productionOrderRoutingOperationMapper.selectById(productionOperationTask.getProductionOrderRoutingOperationId());
        if (routingOperation == null) {
            return;
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOperationTask.getProductionOrderId());
        if (productionOrder == null) {
            return;
        }
        TechnologyRoutingOperation technologyRoutingOperation = technologyRoutingOperationMapper.selectById(routingOperation.getTechnologyRoutingOperationId());
        TechnologyOperation technologyOperation = technologyRoutingOperation == null ? null
                : technologyOperationMapper.selectById(technologyRoutingOperation.getTechnologyOperationId());
        SysUser user = userMapper.selectUserById(productionProductMain.getUserId());
        BigDecimal productQty = defaultDecimal(output.getQuantity());
        if (productQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        ProductionAccount productionAccount = new ProductionAccount();
        productionAccount.setProductionProductMainId(productionProductMain.getId());
        productionAccount.setSchedulingUserId(user == null ? productionProductMain.getUserId() : user.getUserId());
        productionAccount.setSchedulingUserName(user == null ? productionProductMain.getUserName() : user.getNickName());
        productionAccount.setFinishedNum(productQty);
        productionAccount.setWorkHours(technologyOperation != null ? technologyOperation.getSalaryQuota() : null);
        productionAccount.setTechnologyOperationName(technologyOperation == null ? null : technologyOperation.getName());
        productionAccount.setSchedulingDate(LocalDateTime.now());
        productionAccountMapper.insert(productionAccount);
    }
    @Override
    public ArrayList<Long> listMain(List<Long> idList) {
        // 查询主表ID集合