云
2 天以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -1,51 +1,78 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSaveReqVO;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES 生产报工 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -53,57 +80,158 @@
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesFeedbackParamService feedbackParamService;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesMdWorkstationService workstationService;
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    @Lazy // 避免循环依赖
    private MesProTaskService taskService;
    @Resource
    private MesWmItemConsumeService itemConsumeService;
    @Resource
    private MesWmProductIssueService productIssueService;
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
    private MesWmProductProduceLineService produceLineService;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private ApplicationEventPublisher eventPublisher;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
        // 1. 校验
        MesProTaskDO task = validateFeedbackData(createReqVO);
        task = feedbackParamService.lockTask(task.getId());
        validateLockedTaskDimensions(createReqVO, task);
        // 2. 插入(自动填充 itemId)
        // 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
        MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
                .setId(null)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
                .setItemId(task.getItemId())
                .setParamSnapshot(feedbackParamService.buildDraft(task, createReqVO.getParamValues()));
        feedbackMapper.insert(feedback);
        return feedback.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
        // 1.1 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(updateReqVO.getId());
        // 先锁报工再锁任务,与提交顺序一致。
        MesProFeedbackDO original = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, updateReqVO.getId()).last("FOR UPDATE"));
        if (original == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(original.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        // 1.2 校验业务数据
        MesProTaskDO task = validateFeedbackData(updateReqVO);
        task = feedbackParamService.lockTask(task.getId());
        validateLockedTaskDimensions(updateReqVO, task);
        // 2. 更新(自动填充 itemId)
        MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                .setItemId(task.getItemId());
        if (updateReqVO.getParamValues() != null) {
            String snapshot = feedbackParamService.buildDraft(task, updateReqVO.getParamValues());
            updateObj.setParamSnapshot(snapshot == null ? "" : snapshot);
        } else if (!Objects.equals(original.getTaskId(), task.getId())) {
            updateObj.setParamSnapshot("");
        }
        feedbackMapper.updateById(updateObj);
    }
    @Override
    public void deleteFeedback(Long id) {
        // 1. 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(id);
    private void validateLockedTaskDimensions(MesProFeedbackSaveReqVO req, MesProTaskDO task) {
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        if (!Objects.equals(req.getProcessId(), task.getProcessId())
                || !Objects.equals(req.getWorkOrderId(), task.getWorkOrderId())
                || !Objects.equals(req.getRouteId(), task.getRouteId()))
            throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新选择任务");
    }
        // 2. 删除
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackImportRespVO importFeedbackList(List<MesProFeedbackImportExcelVO> importList,
                                                         boolean updateSupport) {
        // 1. 校验非空
        if (CollUtil.isEmpty(importList)) {
            throw exception(MES_PRO_FEEDBACK_IMPORT_LIST_IS_EMPTY);
        }
        // 2. 逐行处理
        MesProFeedbackImportRespVO respVO = MesProFeedbackImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        AtomicInteger index = new AtomicInteger(1);
        importList.forEach(importItem -> {
            int currentIndex = index.getAndIncrement();
            String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " 行");
            if (StrUtil.isBlank(importItem.getCode())) {
                respVO.getFailureCodes().put(key, "报工单编号不能为空");
                return;
            }
            if (StrUtil.isBlank(importItem.getTaskCode())) {
                respVO.getFailureCodes().put(key, "生产任务编码不能为空");
                return;
            }
            // 2.1 解析生产任务
            MesProTaskDO task = taskService.getTaskByCode(importItem.getTaskCode().trim());
            if (task == null) {
                respVO.getFailureCodes().put(key, "生产任务编码[" + importItem.getTaskCode() + "]不存在");
                return;
            }
            // 2.2 构建请求并创建/更新(复用 create/update 服务,报工人/审核人取当前登录用户)
            MesProFeedbackSaveReqVO reqVO = BeanUtils.toBean(importItem, MesProFeedbackSaveReqVO.class);
            reqVO.setTaskId(task.getId());
            Long loginUserId = getLoginUserId();
            reqVO.setFeedbackUserId(ObjUtil.defaultIfNull(reqVO.getFeedbackUserId(), loginUserId));
            reqVO.setApproveUserId(ObjUtil.defaultIfNull(reqVO.getApproveUserId(), loginUserId));
            MesProFeedbackDO existFeedback = feedbackMapper.selectByCode(importItem.getCode());
            try {
                if (existFeedback == null) {
                    reqVO.setId(null);
                    createFeedback(reqVO);
                    respVO.getCreateCodes().add(importItem.getCode());
                } else if (updateSupport) {
                    reqVO.setId(existFeedback.getId());
                    updateFeedback(reqVO);
                    respVO.getUpdateCodes().add(importItem.getCode());
                } else {
                    respVO.getFailureCodes().put(key, "报工单编号已存在");
                }
            } catch (ServiceException e) {
                respVO.getFailureCodes().put(key, e.getMessage());
            }
        });
        return respVO;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteFeedback(Long id) {
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        feedbackMapper.deleteById(id);
    }
@@ -131,19 +259,29 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitFeedback(Long id) {
        // 1. 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(id);
        // 2. 更新状态为审批中(报工人和报工时间由表单保存时确定,提交不覆盖)
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        MesProTaskDO task = feedbackParamService.lockTask(feedback.getTaskId());
        validateTaskAssignee(task);
        String snapshot = feedbackParamService.complete(task, feedback.getParamSnapshot());
        MesProFeedbackDO update = new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
        if (snapshot != null) update.setParamSnapshot(snapshot);
        feedbackMapper.updateById(update);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rejectFeedback(Long id) {
        // 1. 校验存在 + 审批中状态
        validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = lockFeedbackStatusApproving(id);
        // 1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 2. 更新状态为草稿
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
@@ -152,9 +290,11 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean approveFeedback(Long id) {
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = lockFeedbackStatusApproving(reqVO.getId());
        // 1.1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -163,11 +303,10 @@
        // 1.3 校验任务未完成
        taskService.validateTaskNotFinished(feedback.getTaskId());
        // 2.1 查询工序的关键工序标识 + 检验标识(需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0)
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                feedback.getRouteId(), feedback.getProcessId());
        boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag());
        boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag());
        // 2.1 查询工序的检验标识(工单工序快照优先,需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0)
        ProcessFlags processFlags = resolveProcessFlags(feedback.getRouteId(), feedback.getWorkOrderId(),
                feedback.getProcessId());
        boolean checkFlag = processFlags.checkFlag();
        // 2.2 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截)
        if (!checkFlag
                && feedback.getUncheckQuantity() != null
@@ -175,12 +314,29 @@
            throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity());
        }
        if (checkFlag) {
            try {
                templateItemService.getRequiredTemplateByItemIdAndType(feedback.getItemId(), MesQcTypeEnum.IPQC.getType());
            } catch (ServiceException e) {
                if (!Objects.equals(e.getCode(), QC_NO_TEMPLATE.getCode())) throw e;
                MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
                String name = item != null ? item.getName() + "(" + item.getCode() + ")" : "ID " + feedback.getItemId();
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "物料【" + name + "】未配置启用的过程检验模板,请先配置后再审批");
            }
        } else {
            BigDecimal qualified = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO);
            BigDecimal unqualified = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO);
            if (qualified.signum() < 0 || unqualified.signum() < 0
                    || qualified.add(unqualified).compareTo(feedback.getFeedbackQuantity()) != 0) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "合格数和不合格数必须非负且合计等于报工数量");
            }
        }
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        boolean backflushFlag = processFlags.backflushFlag();
        if (backflushFlag) {
            // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料
            List<MesProRouteProductBomDO> boms = routeProductBomService.getRouteProductBomList(
                    feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId());
            // 3.1 校验当前倒冲工序必须配置投料 BOM,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
                    feedback.getWorkOrderId(), feedback.getProcessId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
@@ -188,29 +344,29 @@
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
                // 消耗单负责实际扣库;领料单仅作为倒冲业务凭证,避免重复扣减库存
                productIssueService.generateBackflushProductIssue(feedback, itemConsume);
            }
        }
        // 4. 关键工序:生成产出单,并根据是否需要检验决定入库方式
        if (keyFlag) {
            // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            if (checkFlag) {
                // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + completeFeedbackFromIpqc
                productProduceService.generateProductProduce(feedback, true);
                feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                        .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
                return false;
            }
            // 4.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库,并更新任务/工单的已生产数量
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false);
        // 4. 先生成产出;进度仅在产出完成且报工状态变为完成后更新。
        if (checkFlag) {
            // 6.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
            productProduceService.generateProductProduce(feedback, true, reqVO);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                    .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
            return false;
        } else {
            // 6.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
            feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                    .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()).setUncheckQuantity(BigDecimal.ZERO));
            updateTaskAndWorkOrderByFeedback(feedback);
        }
        // 5. 非关键工序 / 关键非质检工序:直接完成(清零 uncheckQuantity 防止 !key+check 留脏数据)
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus())
                .setUncheckQuantity(BigDecimal.ZERO));
        return true; // 已完成
    }
@@ -221,16 +377,21 @@
     *   <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li>
     *   <li>使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量
     *       (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)</li>
     *      isPush 是否推送生产完成事件
     * </ul>
     *
     * @param feedback 报工单
     */
    private void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
        // 1. 查询该报工单关联的所有产出单行,按质量状态聚合数量
        // 1. 查询该报工单关联的产出单行,只按主产出物料(报工物料)聚合合格/不合格数量,
        //    联产出物料单独检验,不计入任务进度
        BigDecimal qualifiedQty = BigDecimal.ZERO;
        BigDecimal unqualifiedQty = BigDecimal.ZERO;
        List<MesWmProductProduceLineDO> lines = produceLineService.getProductProduceLineListByFeedbackId(feedback.getId());
        for (MesWmProductProduceLineDO line : lines) {
            if (!ObjUtil.equal(line.getItemId(), feedback.getItemId())) {
                continue;
            }
            if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())) {
                unqualifiedQty = unqualifiedQty.add(line.getQuantity());
            }
@@ -239,12 +400,61 @@
            }
        }
        // 2. 更新任务的已生产/合格/不合格数量
        //   2. 更新任务的已生产/合格/不合格数量
        taskService.updateProducedQuantity(feedback.getTaskId(),
                feedback.getFeedbackQuantity(), qualifiedQty, unqualifiedQty);
        // 3. 更新工单的已生产数量
        workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                feedback.getFeedbackQuantity());
        // 3. 更新订单的已生产数量
        // 新增逻辑:只有在该笔订单最后一个工序才更新生产订单的已生产数量
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
        Long lastProcessId;
        if (lastProcess != null) {
            lastProcessId = lastProcess.getProcessId();
        } else {
            // 旧工单可能没有保存工单工序:与排产一致回退到工艺路线工序。
            List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(feedback.getRouteId());
            if (CollUtil.isEmpty(routeProcesses) || routeProcesses.stream().anyMatch(process -> process.getSort() == null)) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法确定末道工序,请检查工单和工艺路线的工序顺序");
            }
            int maxSort = routeProcesses.stream().mapToInt(MesProRouteProcessDO::getSort).max().orElseThrow();
            List<MesProRouteProcessDO> lastProcesses = routeProcesses.stream()
                    .filter(process -> process.getSort() == maxSort).toList();
            if (lastProcesses.size() != 1 || lastProcesses.get(0).getProcessId() == null) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法唯一确定末道工序,请检查工艺路线的工序顺序");
            }
            lastProcessId = lastProcesses.get(0).getProcessId();
        }
        if (ObjUtil.equal(lastProcessId, feedback.getProcessId())) {
            workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                    feedback.getFeedbackQuantity());
            // 4. 更新 MPS 的已生产数量(如果是销售订单来源)
            MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId());
            if (mps != null) {
                mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity());
            }
        }
        // 5. 检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
    }
    /**
     * 检查并发布生产完成事件
     */
    private void checkAndPublishProductionFinished(Long workOrderId) {
        // 1. 获取工单关联的 MPS
        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId);
        if (mps == null || mps.getSaleOrderId() == null) {
            return; // 非销售订单来源的工单,跳过
        }
        // 2. 检查并自动完成 MPS(如果满足条件)
        mpsApi.checkAndFinishMps(mps.getId());
        // 3. 检查销售订单所有生产是否完成
        if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) {
            eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now()));
        }
    }
    // ==================== 校验方法 ====================
@@ -266,6 +476,14 @@
        return feedback;
    }
    private MesProFeedbackDO lockFeedbackStatusApproving(Long id) {
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_APPROVING);
        return feedback;
    }
    private MesProFeedbackDO validateFeedbackStatusApproving(Long id) {
        MesProFeedbackDO feedback = validateFeedbackExists(id);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())) {
@@ -275,26 +493,68 @@
    }
    /**
     * 校验当前登录用户是否为报工单指定的审核人
     *
     * 报工单创建时必须指定 approveUserId,审批通过/驳回只能由该审核人操作,
     * 避免任意拥有菜单权限的用户越权审批。
     *
     * @param feedback 报工单
     */
    private void validateCurrentUserIsApprover(MesProFeedbackDO feedback) {
        if (feedback.getApproveUserId() == null) {
            throw exception(PRO_FEEDBACK_APPROVER_NOT_CONFIGURED);
        }
        if (!feedback.getApproveUserId().equals(getLoginUserId())) {
            throw exception(PRO_FEEDBACK_NOT_APPROVER);
        }
    }
    /**
     * 校验当前登录人是否为任务的报工人;任务未配置报工人时无人可报工
     */
    private void validateTaskAssignee(MesProTaskDO task) {
        Long loginUserId = getLoginUserId();
        if (loginUserId == null) {
            throw exception(PRO_FEEDBACK_NOT_ASSIGNED);
        }
        String current = String.valueOf(loginUserId);
        boolean assigned = StrUtil.split(task.getAssigneeUserIds(), ',')
                .stream().map(String::trim).anyMatch(current::equals);
        if (!assigned) {
            throw exception(PRO_FEEDBACK_NOT_ASSIGNED);
        }
    }
    /**
     * 校验报工单的业务数据(创建 & 修改共用)
     *
     * <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
     * 从任务自动填充,无需前端重复传入。
     *
     * @param reqVO 报工请求
     * @return 关联的生产任务
     */
    private MesProTaskDO validateFeedbackData(MesProFeedbackSaveReqVO reqVO) {
        // 1. 校验工作站存在
        MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId());
        if (ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_WORKSTATION_PROCESS_MISMATCH);
        }
        // 1. 校验任务存在且未终态
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
        // 2.1 校验工艺路线 + 工序配置有效
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                reqVO.getRouteId(), reqVO.getProcessId());
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        // 校验停工状态不能报工
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 2.2 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        // 校验当前登录人是该任务的报工人
        validateTaskAssignee(task);
        // 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取)
        reqVO.setWorkOrderId(task.getWorkOrderId());
        reqVO.setRouteId(task.getRouteId());
        reqVO.setProcessId(task.getProcessId());
        reqVO.setItemId(task.getItemId());
        reqVO.setWorkstationId(task.getWorkstationId());
        // 3. 校验工单工序 / 工艺路线工序配置有效
        boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag();
        // 4.1 校验数量
        if (checkFlag) {
            // 需要检验:只需填报工数量,且必须 > 0
            if (reqVO.getFeedbackQuantity() == null
@@ -310,33 +570,132 @@
            }
        }
        // 3. 校验工单已确认
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        if (ObjUtil.notEqual(workOrder.getProductId(), reqVO.getItemId())) {
            throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH);
        }
        // 4. 校验任务存在且未终态(已完成/已取消),并返回任务用于冗余 itemId
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
        validateTaskRelation(task, workstation, workOrder, reqVO);
        return task;
    }
    private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation,
                                      MesProWorkOrderDO workOrder, MesProFeedbackSaveReqVO reqVO) {
        if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) {
            throw exception(PRO_TASK_WORK_ORDER_MISMATCH);
    /**
     * 解析工序的检验标识与倒冲标识
     *
     * <p>工单工序快照(mes_pro_work_order_process)是下单时确认的权威配置,优先使用;
     * 工单未维护该工序时再回退到工艺路线模板(mes_pro_route_process)。
     */
    private ProcessFlags resolveProcessFlags(Long routeId, Long workOrderId, Long processId) {
        MesProWorkOrderProcessDO workOrderProcess = workOrderId != null && processId != null
                ? workOrderProcessService.getWorkOrderProcessByWorkOrderIdAndProcessId(workOrderId, processId) : null;
        MesProRouteProcessDO routeProcess = routeId != null && processId != null
                ? routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId) : null;
        if (workOrderProcess == null && routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) {
            throw exception(PRO_TASK_WORKSTATION_MISMATCH);
        Boolean checkFlag = workOrderProcess != null && workOrderProcess.getCheckFlag() != null
                ? workOrderProcess.getCheckFlag() : routeProcess != null ? routeProcess.getCheckFlag() : null;
        Boolean backflushFlag = workOrderProcess != null && workOrderProcess.getBackflushFlag() != null
                ? workOrderProcess.getBackflushFlag() : routeProcess != null ? routeProcess.getBackflushFlag() : null;
        return new ProcessFlags(Boolean.TRUE.equals(checkFlag), Boolean.TRUE.equals(backflushFlag));
    }
    private record ProcessFlags(boolean checkFlag, boolean backflushFlag) {
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
        // 1. 校验任务存在且非终态
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
        // 校验停工状态不能报工
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        if (ObjUtil.notEqual(task.getRouteId(), reqVO.getRouteId())
                || ObjUtil.notEqual(task.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH);
        // 校验当前登录人是该任务的报工人
        validateTaskAssignee(task);
        // 2. 校验工单工序 / 工艺路线工序配置
        boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag();
        // 3. 校验数量
        if (checkFlag) {
            if (reqVO.getFeedbackQuantity() == null
                    || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
            }
        } else {
            BigDecimal qualified = ObjectUtil.defaultIfNull(reqVO.getQualifiedQuantity(), BigDecimal.ZERO);
            BigDecimal unqualified = ObjectUtil.defaultIfNull(reqVO.getUnqualifiedQuantity(), BigDecimal.ZERO);
            if (qualified.add(unqualified).compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED);
            }
        }
        if (ObjUtil.notEqual(task.getItemId(), reqVO.getItemId())) {
            throw exception(PRO_TASK_ITEM_MISMATCH);
        // 4. 构建报工DO
        MesProFeedbackDO feedback = new MesProFeedbackDO()
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
                .setType(MesProFeedbackTypeEnum.SELF.getType())
                .setWorkstationId(task.getWorkstationId())
                .setRouteId(task.getRouteId())
                .setProcessId(task.getProcessId())
                .setWorkOrderId(task.getWorkOrderId())
                .setTaskId(reqVO.getTaskId())
                .setItemId(task.getItemId())
                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
                .setQualifiedQuantity(reqVO.getQualifiedQuantity())
                .setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity())
                .setScheduledQuantity(task.getQuantity())
                .setFeedbackTime(LocalDateTime.now())
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                .setRemark(reqVO.getRemark());
        MesProTaskDO lockedTask = feedbackParamService.lockTask(task.getId());
        if (!MesProTaskStatusEnum.canFeedback(lockedTask.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        if (!Objects.equals(task.getProcessId(), lockedTask.getProcessId())
                || !Objects.equals(task.getWorkOrderId(), lockedTask.getWorkOrderId())
                || !Objects.equals(task.getRouteId(), lockedTask.getRouteId()))
            throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新报工");
        String draftParam = feedbackParamService.buildDraft(lockedTask, reqVO.getParamValues());
        feedback.setParamSnapshot(feedbackParamService.complete(lockedTask, draftParam));
        feedbackMapper.insert(feedback);
        // 5. 返回结果
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId());
        return new MesProFeedbackQuickCreateRespVO()
                .setFeedbackId(feedback.getId())
                .setTaskStatus(task.getStatus())
                .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) {
        List<MesProFeedbackBatchCreateRespVO.BatchResult> results = new java.util.ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (Long taskId : reqVO.getTaskIds()) {
            try {
                MesProTaskDO batchTask = feedbackParamService.lockTask(taskId);
                if (Objects.equals(batchTask.getParamCollectMode(), 1)
                        || (Objects.equals(batchTask.getParamCollectMode(), 2) && StrUtil.isBlank(batchTask.getParamSharedSnapshot()))) {
                    throw exception(PRO_FEEDBACK_PARAM_INVALID, "该任务需要单独填写参数,请使用单笔报工");
                }
                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
                singleReq.setTaskId(taskId);
                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
                singleReq.setQualifiedQuantity(reqVO.getQualifiedQuantity());
                singleReq.setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity());
                singleReq.setRemark(reqVO.getRemark());
                MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
                        .setTaskId(taskId)
                        .setSuccess(true)
                        .setFeedbackId(resp.getFeedbackId()));
                successCount++;
            } catch (Exception e) {
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
                        .setTaskId(taskId)
                        .setSuccess(false)
                        .setErrorMsg(e.getMessage()));
                failCount++;
            }
        }
        return new MesProFeedbackBatchCreateRespVO()
                .setSuccessCount(successCount)
                .setFailCount(failCount)
                .setResults(results);
    }
    @Override
@@ -345,27 +704,218 @@
                                                BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
                                                BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
        // 1. 校验报工单存在且为待检验状态
        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, feedbackId).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_UNCHECK);
        }
        // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
        // 2. 拆分当前检验的产出行(合格/不合格),生成明细;全部物料检验完成才完成产出入库
        productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty);
        // 3. 回写合格/不合格/废品数量,更新状态为已完成
        feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
                .setQualifiedQuantity(qualifiedQty)
                .setUnqualifiedQuantity(unqualifiedQty)
                .setUncheckQuantity(BigDecimal.ZERO)
                .setLaborScrapQuantity(laborScrapQty)
                .setMaterialScrapQuantity(materialScrapQty)
                .setOtherScrapQuantity(otherScrapQty)
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
        // 3. 累加本次检验结果;报工单只记录主产出物料的合格/不合格/废品数量,
        //    联产出物料各自检验,不并入报工数量;待全部产出物料检验完成才完成报工
        List<MesWmProductProduceLineDO> produceLines = produceLineService.getProductProduceLineListByFeedbackId(feedbackId);
        MesWmProductProduceLineDO sourceLine = produceLines.stream()
                .filter(line -> ObjUtil.equal(line.getId(), sourceLineId)).findFirst().orElse(null);
        boolean mainOutput = sourceLine == null || ObjUtil.equal(sourceLine.getItemId(), feedback.getItemId());
        BigDecimal totalQualified = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? qualifiedQty : BigDecimal.ZERO);
        BigDecimal totalUnqualified = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? unqualifiedQty : BigDecimal.ZERO);
        BigDecimal totalLaborScrap = ObjectUtil.defaultIfNull(feedback.getLaborScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? laborScrapQty : BigDecimal.ZERO);
        BigDecimal totalMaterialScrap = ObjectUtil.defaultIfNull(feedback.getMaterialScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? materialScrapQty : BigDecimal.ZERO);
        BigDecimal totalOtherScrap = ObjectUtil.defaultIfNull(feedback.getOtherScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? otherScrapQty : BigDecimal.ZERO);
        boolean allInspected = produceLines.stream()
                .noneMatch(line -> ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()));
        MesProFeedbackDO updateObj = new MesProFeedbackDO().setId(feedbackId)
                .setQualifiedQuantity(totalQualified)
                .setUnqualifiedQuantity(totalUnqualified)
                .setLaborScrapQuantity(totalLaborScrap)
                .setMaterialScrapQuantity(totalMaterialScrap)
                .setOtherScrapQuantity(totalOtherScrap)
                .setStatus(allInspected ? MesProFeedbackStatusEnum.FINISHED.getStatus()
                        : MesProFeedbackStatusEnum.UNCHECK.getStatus());
        if (allInspected) {
            updateObj.setUncheckQuantity(BigDecimal.ZERO);
        }
        feedbackMapper.updateById(updateObj);
        // 4. 更新任务/工单的已生产数量
        feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
        updateTaskAndWorkOrderByFeedback(feedback);
        // 4. 全部检验完成后回写任务/工单进度
        if (allInspected) {
            updateTaskAndWorkOrderByFeedback(feedback);
        }
    }
    @Override
    public MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId) {
        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
        MesProFeedbackBatchCheckRespVO respVO = new MesProFeedbackBatchCheckRespVO();
        // 1. 检查物料是否启用批次管理
        MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
        if (item == null || Boolean.FALSE.equals(item.getIsBatchManaged())) {
            respVO.setNeedBatchInput(false);
            respVO.setIsBatchManaged(false);
            return respVO;
        }
        respVO.setIsBatchManaged(true);
        // 2. 获取批次配置
        MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(feedback.getItemId());
        if (config == null) {
            respVO.setNeedBatchInput(false);
            return respVO;
        }
        // 3. 填充配置标志
        respVO.setProduceDateFlag(config.getProduceDateFlag());
        respVO.setExpireDateFlag(config.getExpireDateFlag());
        respVO.setReceiptDateFlag(config.getReceiptDateFlag());
        respVO.setVendorFlag(config.getVendorFlag());
        respVO.setClientFlag(config.getClientFlag());
        respVO.setSalesOrderCodeFlag(config.getSalesOrderCodeFlag());
        respVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag());
        respVO.setWorkOrderFlag(config.getWorkOrderFlag());
        respVO.setTaskFlag(config.getTaskFlag());
        respVO.setWorkstationFlag(config.getWorkstationFlag());
        respVO.setToolFlag(config.getToolFlag());
        respVO.setMoldFlag(config.getMoldFlag());
        respVO.setLotNumberFlag(config.getLotNumberFlag());
        respVO.setQualityStatusFlag(config.getQualityStatusFlag());
        // 4. 判断哪些字段已自动填充(报工和生产过程中必然有值的字段)
        List<String> autoFilled = new ArrayList<>();
        // 生产日期:审批时自动取当天
        autoFilled.add("produceDate");
        // 生产工单:从报工记录获取
        autoFilled.add("workOrderId");
        // 生产任务:从报工记录获取
        autoFilled.add("taskId");
        // 工作站:从报工记录获取
        autoFilled.add("workstationId");
        respVO.setAutoFilledFields(autoFilled);
        // 5. 判断是否需要弹窗:存在必填但无法自动填充的字段
        boolean needBatchInput = false;
        if (Boolean.TRUE.equals(config.getExpireDateFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getReceiptDateFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getVendorFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getClientFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getToolFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getMoldFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getLotNumberFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getQualityStatusFlag())) needBatchInput = true;
        respVO.setNeedBatchInput(needBatchInput);
        return respVO;
    }
    @Override
    public MesProFeedbackSummaryRespVO getFeedbackSummary(LocalDate beginDate, LocalDate endDate,
                                                          Integer workOrderType) {
        // 1. 查询有效报工(已批准:待检验 + 已完成)
        LambdaQueryWrapperX<MesProFeedbackDO> query = new LambdaQueryWrapperX<MesProFeedbackDO>()
                .in(MesProFeedbackDO::getStatus, List.of(
                        MesProFeedbackStatusEnum.UNCHECK.getStatus(),
                        MesProFeedbackStatusEnum.FINISHED.getStatus()))
                .betweenIfPresent(MesProFeedbackDO::getFeedbackTime,
                        beginDate != null ? beginDate.atStartOfDay() : null,
                        endDate != null ? endDate.plusDays(1).atStartOfDay() : null);
        List<MesProFeedbackDO> feedbackList = feedbackMapper.selectList(query);
        if (CollUtil.isEmpty(feedbackList)) {
            MesProFeedbackSummaryRespVO empty = new MesProFeedbackSummaryRespVO();
            empty.setTotalFeedbackQuantity(BigDecimal.ZERO);
            empty.setTotalQualifiedQuantity(BigDecimal.ZERO);
            empty.setTotalFeedbackCount(0);
            empty.setByType(Collections.emptyList());
            empty.setByDate(Collections.emptyList());
            return empty;
        }
        // 2. 加载工单信息,映射 工单ID -> 工单类型
        Set<Long> workOrderIds = feedbackList.stream()
                .map(MesProFeedbackDO::getWorkOrderId).filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, MesProWorkOrderDO> workOrderMap = workOrderService.getWorkOrderMap(workOrderIds);
        // 3. 过滤工单类型
        List<MesProFeedbackDO> filteredList = feedbackList.stream()
                .filter(feedback -> {
                    if (workOrderType == null) {
                        return true;
                    }
                    MesProWorkOrderDO workOrder = workOrderMap.get(feedback.getWorkOrderId());
                    return workOrder != null && Objects.equals(workOrder.getType(), workOrderType);
                })
                .collect(Collectors.toList());
        // 4. 汇总
        BigDecimal totalFeedbackQuantity = BigDecimal.ZERO;
        BigDecimal totalQualifiedQuantity = BigDecimal.ZERO;
        int totalCount = 0;
        for (MesProFeedbackDO feedback : filteredList) {
            totalFeedbackQuantity = totalFeedbackQuantity.add(nullToZero(feedback.getFeedbackQuantity()));
            totalQualifiedQuantity = totalQualifiedQuantity.add(nullToZero(feedback.getQualifiedQuantity()));
            totalCount++;
        }
        // 5. 按工单类型分组
        Map<Integer, MesProFeedbackSummaryRespVO.TypeSummaryItem> typeMap = new LinkedHashMap<>();
        // 6. 按日期分组
        Map<LocalDate, MesProFeedbackSummaryRespVO.DateSummaryItem> dateMap = new LinkedHashMap<>();
        for (MesProFeedbackDO feedback : filteredList) {
            MesProWorkOrderDO workOrder = workOrderMap.get(feedback.getWorkOrderId());
            Integer type = workOrder != null ? workOrder.getType() : null;
            if (type != null) {
                MesProFeedbackSummaryRespVO.TypeSummaryItem item = typeMap.computeIfAbsent(type, key -> {
                    MesProFeedbackSummaryRespVO.TypeSummaryItem t = new MesProFeedbackSummaryRespVO.TypeSummaryItem();
                    t.setWorkOrderType(key);
                    t.setWorkOrderTypeName(MesProWorkOrderTypeEnum.valueOf(key) != null
                            ? MesProWorkOrderTypeEnum.valueOf(key).getName() : String.valueOf(key));
                    t.setFeedbackQuantity(BigDecimal.ZERO);
                    t.setQualifiedQuantity(BigDecimal.ZERO);
                    t.setFeedbackCount(0);
                    return t;
                });
                item.setFeedbackQuantity(item.getFeedbackQuantity().add(nullToZero(feedback.getFeedbackQuantity())));
                item.setQualifiedQuantity(item.getQualifiedQuantity().add(nullToZero(feedback.getQualifiedQuantity())));
                item.setFeedbackCount(item.getFeedbackCount() + 1);
            }
            if (feedback.getFeedbackTime() != null) {
                LocalDate date = feedback.getFeedbackTime().toLocalDate();
                MesProFeedbackSummaryRespVO.DateSummaryItem item = dateMap.computeIfAbsent(date, key -> {
                    MesProFeedbackSummaryRespVO.DateSummaryItem d = new MesProFeedbackSummaryRespVO.DateSummaryItem();
                    d.setDate(key);
                    d.setFeedbackQuantity(BigDecimal.ZERO);
                    d.setQualifiedQuantity(BigDecimal.ZERO);
                    d.setFeedbackCount(0);
                    return d;
                });
                item.setFeedbackQuantity(item.getFeedbackQuantity().add(nullToZero(feedback.getFeedbackQuantity())));
                item.setQualifiedQuantity(item.getQualifiedQuantity().add(nullToZero(feedback.getQualifiedQuantity())));
                item.setFeedbackCount(item.getFeedbackCount() + 1);
            }
        }
        // 7. 组装响应
        MesProFeedbackSummaryRespVO respVO = new MesProFeedbackSummaryRespVO();
        respVO.setTotalFeedbackQuantity(totalFeedbackQuantity);
        respVO.setTotalQualifiedQuantity(totalQualifiedQuantity);
        respVO.setTotalFeedbackCount(totalCount);
        respVO.setByType(new ArrayList<>(typeMap.values()));
        respVO.setByDate(new ArrayList<>(dateMap.values()));
        return respVO;
    }
    private static BigDecimal nullToZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}