云
2026-10-25 d54fbc9d4f8981fa0f7f613b33a28a8cd4f764ea
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -1,14 +1,17 @@
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.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.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;
@@ -29,6 +32,7 @@
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;
@@ -46,13 +50,21 @@
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.*;
/**
@@ -80,6 +92,8 @@
    private MesProTaskService taskService;
    @Resource
    private MesWmItemConsumeService itemConsumeService;
    @Resource
    private MesWmProductIssueService productIssueService;
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
@@ -119,6 +133,63 @@
        MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                .setItemId(task.getItemId());
        feedbackMapper.updateById(updateObj);
    }
    @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
@@ -166,7 +237,9 @@
    @Override
    public void rejectFeedback(Long id) {
        // 1. 校验存在 + 审批中状态
        validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
        // 1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 2. 更新状态为草稿
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
@@ -178,6 +251,8 @@
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        // 1.1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -201,9 +276,9 @@
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        if (backflushFlag) {
            // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                    feedback.getWorkOrderId());
            // 3.1 校验当前倒冲工序必须配置投料 BOM,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
                    feedback.getWorkOrderId(), feedback.getProcessId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
@@ -211,6 +286,8 @@
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
                // 消耗单负责实际扣库;领料单仅作为倒冲业务凭证,避免重复扣减库存
                productIssueService.generateBackflushProductIssue(feedback, itemConsume);
            }
        }
@@ -340,6 +417,23 @@
            throw exception(PRO_FEEDBACK_NOT_APPROVING);
        }
        return feedback;
    }
    /**
     * 校验当前登录用户是否为报工单指定的审核人
     *
     * 报工单创建时必须指定 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);
        }
    }
    /**
@@ -580,4 +674,105 @@
        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;
    }
}