2026-04-23 a1b154bfd4c5e138d964e1bfdc5a2bcac1e25488
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -1,285 +1,246 @@
package com.ruoyi.production.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.production.bean.dto.ProductionPlanDto;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.dto.ProductionPlanImportDto;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.production.service.ProductionPlanService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ProductionPlanServiceImpl extends ServiceImpl<ProductionPlanMapper, ProductionPlan> implements ProductionPlanService {
    private final ProductionOrderService productionOrderService;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    @Override
    public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
        Page<ProductionPlan> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        return this.page(entityPage, buildQueryWrapper(productionPlanDto))
                .convert(item -> BeanUtil.copyProperties(item, ProductionPlanVo.class));
        IPage<ProductionPlanVo> planVoIPage = productionPlanMapper.listPage(page, productionPlanDto)
                .convert(dto -> {
                    ProductionPlanVo vo = new ProductionPlanVo();
                    BeanUtils.copyProperties(dto, vo);
                    return vo;
                });
        return planVoIPage;
    }
    /**
     * 合并生产计划
     */
    @Override
    public void loadProdData() {
        // 用销售明细作为来源同步生产计划,source 字段承担幂等去重作用。
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(
                Wrappers.<SalesLedgerProduct>lambdaQuery()
                        .isNotNull(SalesLedgerProduct::getProductModelId)
                        .gt(SalesLedgerProduct::getQuantity, BigDecimal.ZERO));
        for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
            String source = buildSalesSource(salesLedgerProduct.getId());
            long exists = this.count(Wrappers.<ProductionPlan>lambdaQuery().eq(ProductionPlan::getSource, source));
            if (exists > 0) {
                continue;
            }
            SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerProduct.getSalesLedgerId());
            ProductionPlan productionPlan = new ProductionPlan();
            productionPlan.setMpsNo(generateNextPlanNo());
            productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
            productionPlan.setQtyRequired(defaultDecimal(salesLedgerProduct.getQuantity()));
            productionPlan.setRequiredDate(resolveDeliveryTime(salesLedger));
            productionPlan.setPromisedDeliveryDate(resolveDeliveryTime(salesLedger));
            productionPlan.setSource(source);
            productionPlan.setIssued(Boolean.FALSE);
            productionPlan.setState("1");
            this.save(productionPlan);
        }
    }
    @Override
    public void syncProdDataJob() {
        loadProdData();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean combine(ProductionPlanDto productionPlanDto) {
        // 多个计划合并转单后,仍统一走生产订单保存逻辑,避免两套下单流程不一致。
        if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
            throw new ServiceException("请选择生产计划");
        }
        List<ProductionPlan> productionPlans = this.listByIds(productionPlanDto.getIds());
        if (productionPlans.size() != productionPlanDto.getIds().size()) {
            throw new ServiceException("部分生产计划不存在");
        }
        if (productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued()))) {
            throw new ServiceException("已下发的生产计划不能重复转单");
        }
        Long productModelId = productionPlans.stream()
                .map(ProductionPlan::getProductModelId)
                .distinct()
                .reduce((left, right) -> {
                    throw new ServiceException("仅支持相同产品规格的生产计划合并转单");
                })
                .orElseThrow(() -> new ServiceException("生产计划缺少产品规格"));
        BigDecimal totalRequiredQuantity = productionPlans.stream()
                .map(ProductionPlan::getQtyRequired)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal assignedQuantity = defaultDecimal(productionPlanDto.getTotalAssignedQuantity());
        if (assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            assignedQuantity = totalRequiredQuantity;
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发数量不能大于计划总需求数量");
        }
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setProductModelId(productModelId);
        productionOrder.setQuantity(assignedQuantity);
        productionOrder.setProductionPlanIds(formatPlanIds(productionPlans));
        productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime() != null
                ? productionPlanDto.getPlanCompleteTime()
                : resolveEarliestPlanDate(productionPlans));
        productionOrder.setStrength(productionPlanDto.getStrength());
        return productionOrderService.saveProductionOrder(productionOrder);
    }
    @Override
    public boolean add(ProductionPlanDto productionPlanDto) {
        // 手工建计划时补齐默认编号和状态,保证后续可以直接下发转单。
        ProductionPlan productionPlan = BeanUtil.copyProperties(productionPlanDto, ProductionPlan.class);
        validateProductionPlan(productionPlan, false);
        if (productionPlan.getMpsNo() == null || productionPlan.getMpsNo().trim().isEmpty()) {
            productionPlan.setMpsNo(generateNextPlanNo());
        }
        if (productionPlan.getIssued() == null) {
            productionPlan.setIssued(Boolean.FALSE);
        }
        if (productionPlan.getState() == null || productionPlan.getState().trim().isEmpty()) {
            productionPlan.setState("1");
        }
        return this.save(productionPlan);
    }
    @Override
    public boolean update(ProductionPlanDto productionPlanDto) {
        // 已下发计划的核心字段不能再变更,否则会和已生成订单脱节。
        if (productionPlanDto == null || productionPlanDto.getId() == null) {
            throw new ServiceException("生产计划ID不能为空");
        }
        ProductionPlan current = this.getById(productionPlanDto.getId());
        if (current == null) {
            throw new ServiceException("生产计划不存在");
        }
        if (Boolean.TRUE.equals(current.getIssued()) && hasPlanCoreChanges(current, productionPlanDto)) {
            throw new ServiceException("已下发的生产计划不允许修改产品、数量和交期");
        }
        ProductionPlan update = BeanUtil.copyProperties(productionPlanDto, ProductionPlan.class);
        validateProductionPlan(update, true);
        return this.updateById(update);
    }
    @Override
    public boolean delete(List<Long> ids) {
        // 删除前校验 issued,避免把已转单计划直接从源头删掉。
        if (ids == null || ids.isEmpty()) {
        if (productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
            return false;
        }
        List<ProductionPlan> productionPlans = this.listByIds(ids);
        if (productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued()))) {
            throw new ServiceException("已下发的生产计划不允许删除");
        //  查询主生产计划
        List<ProductionPlanDto> plans = productionPlanMapper.selectWithMaterialByIds(productionPlanDto.getIds());
        if (plans == null || plans.isEmpty()) {
            throw new ServiceException("下发失败,生产计划不存在");
        }
        return this.removeByIds(ids);
        //  校验是否存在不同的产品名称
        String firstProductName = plans.get(0).getProductName();
        if (plans.stream().anyMatch(p -> p.getProductName() == null || !p.getProductName().equals(firstProductName))) {
            throw new BaseException("合并失败,存在不同的产品名称");
        }
        // 校验是否存在不同的产品规格
        String firstProductSpec = plans.get(0).getModel();
        if (plans.stream().anyMatch(p -> p.getModel() == null || !p.getModel().equals(firstProductSpec))) {
            throw new BaseException("合并失败,存在不同的产品规格");
        }
        // 创建生产订单
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setQuantity(productionPlanDto.getTotalAssignedQuantity());
        productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
        productionOrder.setStatus(ProductOrderStatusEnum.WAIT.getCode());
        productionOrder.setStrength(productionPlanDto.getStrength());
        return true;
    }
    @Override
    public void importProdData(MultipartFile file) {
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ProductionPlanDto dto) {
        if (StringUtils.isBlank(dto.getApplyNo())) {
            throw new ServiceException("新增失败,申请单编号不能为空");
        }
        checkApplyNoUnique(dto.getApplyNo(), null);
        dto.setStatus(0);
        return productionPlanMapper.insert(dto) > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean update(ProductionPlanDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("编辑失败,数据不能为空");
        }
        ProductionPlan old = getById(dto.getId());
        if (old == null) {
            throw new ServiceException("编辑失败,主生产计划不存在");
        }
        // 状态校验
        if (old.getStatus() != 0) {
            throw new BaseException("编辑失败,该生产计划已下发或部分下发状态,禁止编辑");
        }
        // applyNo变更才校验
        if (StringUtils.isNotBlank(dto.getApplyNo())
                && !dto.getApplyNo().equals(old.getApplyNo())) {
            checkApplyNoUnique(dto.getApplyNo(), dto.getId());
        }
        return productionPlanMapper.updateById(dto) > 0;
    }
    private void checkApplyNoUnique(String applyNo, Long excludeId) {
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getApplyNo, applyNo);
        if (excludeId != null) {
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            throw new ServiceException("申请单编号 " + applyNo + " 已存在");
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean delete(List<Long> ids) {
        // 如果存在已下发的计划,则不能删除
        if (productionPlanMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getId, ids)).stream().anyMatch(p -> p.getStatus() == 1 || p.getStatus() == 2)) {
            throw new BaseException("删除失败,存在已下发或部分下发的计划");
        }
        // 如果有关联订单,则不能删除
        if (productionOrderMapper.selectList(Wrappers.<ProductionOrder>lambdaQuery().in(ProductionOrder::getProductionPlanIds, ids)).stream().anyMatch(p -> p.getId() != null)) {
            throw new BaseException("删除失败,存在关联订单");
        }
        return productionPlanMapper.deleteBatchIds(ids) > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importProdData(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new ServiceException("导入数据不能为空");
        }
        ExcelUtil<ProductionPlanImportDto> excelUtil = new ExcelUtil<>(ProductionPlanImportDto.class);
        List<ProductionPlanImportDto> list;
        try {
            list = excelUtil.importExcel(file.getInputStream());
        } catch (Exception e) {
            log.error("生产需求Excel导入失败", e);
            throw new ServiceException("Excel解析失败");
        }
        if (list == null || list.isEmpty()) {
            throw new ServiceException("Excel没有数据");
        }
        Set<String> applyNos = new HashSet<>();
        Set<String> materialCodes = new HashSet<>();
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            String applyNo = dto.getApplyNo();
            String materialCode = dto.getMaterialCode();
            if (StringUtils.isEmpty(applyNo)) {
                throw new ServiceException("导入失败:第 " + (i + 2) + " 行申请单编号不能为空");
            }
            if (!applyNos.add(applyNo)) {
                throw new ServiceException("导入失败:Excel 中存在重复的申请单编号: " + applyNo);
            }
            if (StringUtils.isEmpty(materialCode)) {
                throw new ServiceException("导入失败:第 " + (i + 2) + " 行物料编码不能为空");
            }
            String strength = dto.getStrength();
            if (StringUtils.isNotEmpty(strength)) {
                if (!"A3.5".equals(strength) && !"A5.0".equals(strength)) {
                    throw new ServiceException("导入失败:第 " + (i + 2) + " 行强度只能是 A3.5 或 A5.0");
                }
            }
            materialCodes.add(materialCode);
        }
        //  申请单编号是否已存在
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery()
                .in(ProductionPlan::getApplyNo, applyNos));
        if (existApplyNoCount > 0) {
            List<String> existApplyNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery()
                            .in(ProductionPlan::getApplyNo, applyNos))
                    .stream().map(ProductionPlan::getApplyNo).collect(Collectors.toList());
            throw new ServiceException("导入失败,申请单编号已存在: " + String.join(", ", existApplyNos));
        }
        LocalDateTime now = LocalDateTime.now();
        List<ProductionPlan> entityList = list.stream().map(dto -> {
            ProductionPlan entity = new ProductionPlan();
            BeanUtils.copyProperties(dto, entity);
            entity.setStatus(0);
            entity.setCreateTime(now);
            entity.setUpdateTime(now);
            return entity;
        }).collect(Collectors.toList());
        this.saveBatch(entityList);
    }
    @Override
    public void exportProdData(HttpServletResponse response, List<Long> ids) {
    }
    private LambdaQueryWrapper<ProductionPlan> buildQueryWrapper(ProductionPlanDto dto) {
        ProductionPlan query = dto == null ? new ProductionPlan() : dto;
        return Wrappers.<ProductionPlan>lambdaQuery()
                .eq(query.getId() != null, ProductionPlan::getId, query.getId())
                .eq(query.getProductModelId() != null, ProductionPlan::getProductModelId, query.getProductModelId())
                .eq(query.getIssued() != null, ProductionPlan::getIssued, query.getIssued())
                .eq(query.getState() != null && !query.getState().trim().isEmpty(), ProductionPlan::getState, query.getState())
                .eq(query.getIsAudit() != null && !query.getIsAudit().trim().isEmpty(), ProductionPlan::getIsAudit, query.getIsAudit())
                .like(query.getMpsNo() != null && !query.getMpsNo().trim().isEmpty(), ProductionPlan::getMpsNo, query.getMpsNo())
                .like(query.getSource() != null && !query.getSource().trim().isEmpty(), ProductionPlan::getSource, query.getSource())
                .orderByDesc(ProductionPlan::getId);
    }
    private void validateProductionPlan(ProductionPlan productionPlan, boolean update) {
        if (productionPlan == null) {
            throw new ServiceException("生产计划不能为空");
        List<ProductionPlan> list;
        if (ids != null && !ids.isEmpty()) {
            list = baseMapper.selectBatchIds(ids);
        } else {
            list = baseMapper.selectList(null);
        }
        if (update && productionPlan.getId() == null) {
            throw new ServiceException("生产计划ID不能为空");
        List<ProductionPlanImportDto> exportList = new ArrayList<>();
        for (ProductionPlan entity : list) {
            ProductionPlanImportDto dto = new ProductionPlanImportDto();
            BeanUtils.copyProperties(entity, dto);
            exportList.add(dto);
        }
        if (productionPlan.getProductModelId() == null) {
            throw new ServiceException("productModelId不能为空");
        }
        if (defaultDecimal(productionPlan.getQtyRequired()).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("qtyRequired必须大于0");
        }
    }
    private boolean hasPlanCoreChanges(ProductionPlan current, ProductionPlanDto update) {
        // 这些字段会直接影响转单结果,用来判断是否属于核心变更。
        return !Objects.equals(current.getProductModelId(), update.getProductModelId())
                || defaultDecimal(current.getQtyRequired()).compareTo(defaultDecimal(update.getQtyRequired())) != 0
                || !Objects.equals(toLocalDate(current.getPromisedDeliveryDate()), update.getPlanCompleteTime())
                || !Objects.equals(toLocalDate(current.getRequiredDate()), toLocalDate(update.getRequiredDate()));
    }
    private String generateNextPlanNo() {
        // 编号按日期递增生成,方便和订单、工单统一追踪。
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "MPS" + datePrefix;
        ProductionPlan latestPlan = this.getOne(Wrappers.<ProductionPlan>lambdaQuery()
                .likeRight(ProductionPlan::getMpsNo, prefix)
                .orderByDesc(ProductionPlan::getMpsNo)
                .last("limit 1"));
        int sequence = 1;
        if (latestPlan != null && latestPlan.getMpsNo() != null && latestPlan.getMpsNo().startsWith(prefix)) {
            try {
                sequence = Integer.parseInt(latestPlan.getMpsNo().substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                sequence = 1;
            }
        }
        return prefix + String.format("%04d", sequence);
    }
    private String formatPlanIds(List<ProductionPlan> productionPlans) {
        return productionPlans.stream()
                .map(ProductionPlan::getId)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    }
    private LocalDate resolveEarliestPlanDate(List<ProductionPlan> productionPlans) {
        return productionPlans.stream()
                .map(this::resolvePlanDate)
                .filter(Objects::nonNull)
                .min(Comparator.naturalOrder())
                .orElse(null);
    }
    private LocalDate resolvePlanDate(ProductionPlan productionPlan) {
        if (productionPlan.getPromisedDeliveryDate() != null) {
            return productionPlan.getPromisedDeliveryDate().toLocalDate();
        }
        if (productionPlan.getRequiredDate() != null) {
            return productionPlan.getRequiredDate().toLocalDate();
        }
        return null;
    }
    private LocalDateTime resolveDeliveryTime(SalesLedger salesLedger) {
        if (salesLedger == null || salesLedger.getDeliveryDate() == null) {
            return null;
        }
        return salesLedger.getDeliveryDate().atStartOfDay();
    }
    private String buildSalesSource(Long salesLedgerProductId) {
        return "salesLedgerProduct:" + salesLedgerProductId;
    }
    private LocalDate toLocalDate(LocalDateTime value) {
        return value == null ? null : value.toLocalDate();
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
        ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class);
        util.exportExcel(response, exportList, "销售生产需求数据");
    }
}