liyong
5 天以前 2cffa027595247c136a8ee95f5f745c58cb0cce5
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -6,6 +6,10 @@
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.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.exception.ServiceException;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
@@ -44,27 +48,32 @@
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderService productionOrderService;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    @Override
    public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
        // 分页查询主生产计划列表
        return productionPlanMapper.listPage(page, productionPlanDto);
    }
    /**
     * 合并生产计划并下发生产订单。
     * 业务约束:
     * 约束:
     * 1. 仅允许同一产品型号的计划合并;
     * 2. 已下发或部分下发的计划禁止再次合并;
     * 3. 下发数量不能大于所选计划需求总量;
     * 4. 订单创建统一复用 ProductionOrderService.saveProductionOrder,确保工艺/BOM/领料主单等后续逻辑一致。
     * 2. 已下发或部分下发的计划不允许再次合并;
     * 3. 下发数量不能大于所选计划剩余需求总量;
     * 4. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean combine(ProductionPlanDto productionPlanDto) {
        // 基础入参校验:没有可下发计划则直接返回 false
        if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
            return false;
        }
        // 去空、去重,得到本次参与合并的计划 ID
        List<Long> planIds = productionPlanDto.getIds().stream()
                .filter(Objects::nonNull)
                .distinct()
@@ -73,63 +82,105 @@
            throw new ServiceException("下发失败,未选择生产计划");
        }
        List<ProductionPlanDto> plans = productionPlanMapper.selectWithMaterialByIds(planIds);
        if (plans == null || plans.isEmpty() || plans.size() != planIds.size()) {
        // 查询并校验计划是否都存在
        List<ProductionPlanDto> planLists = productionPlanMapper.selectWithMaterialByIds(planIds);
        if (planLists == null || planLists.isEmpty() || planLists.size() != planIds.size()) {
            throw new ServiceException("下发失败,生产计划不存在或已被删除");
        }
        ProductionPlanDto firstPlan = plans.getFirst();
        // 以第一条计划作为型号基准
        ProductionPlanDto firstPlan = planLists.getFirst();
        if (firstPlan.getProductModelId() == null) {
            throw new ServiceException("下发失败,生产计划缺少产品型号");
        }
        boolean hasDifferentModel = plans.stream()
        // 仅允许同型号计划合并下发
        boolean hasDifferentModel = planLists.stream()
                .anyMatch(item -> !Objects.equals(item.getProductModelId(), firstPlan.getProductModelId()));
        if (hasDifferentModel) {
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        boolean hasIssuedPlan = plans.stream()
                .anyMatch(item -> item.getStatus() != null && item.getStatus() != PLAN_STATUS_WAIT);
        if (hasIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发或部分下发数据");
        // 仅“已下发”计划不允许再次参与合并下发;
        // “待下发/部分下发”允许继续下发剩余数量。
        boolean hasFullyIssuedPlan = planLists.stream()
                .anyMatch(item -> item.getStatus() != null
                        && item.getStatus() == PLAN_STATUS_ISSUED);
        if (hasFullyIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发的数据");
        }
        BigDecimal totalRequiredQuantity = plans.stream()
                .map(ProductionPlan::getQtyRequired)
                .filter(Objects::nonNull)
        // 计算本次可下发的剩余需求总量
        BigDecimal totalRequiredQuantity = planLists.stream()
                .map(this::resolveRemainingQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,所选生产计划需求总量必须大于0");
            throw new ServiceException("下发失败,所选生产计划剩余需求总量必须大于0");
        }
        // 校验下发数量
        BigDecimal assignedQuantity = productionPlanDto.getTotalAssignedQuantity();
        if (assignedQuantity == null || assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,下发数量必须大于0");
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发失败,下发数量不能大于计划需求总量");
            throw new ServiceException("下发失败,下发数量不能大于计划剩余需求总量");
        }
        // 按计划顺序分摊下发数量,收集实际参与下发的计划 ID
        BigDecimal remainingForOrderBind = assignedQuantity;
        List<Long> issuedPlanIds = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            if (remainingForOrderBind.compareTo(BigDecimal.ZERO) <= 0 || remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal issueForThisPlan = remainingForOrderBind.min(remainingQuantity);
            remainingForOrderBind = remainingForOrderBind.subtract(issueForThisPlan);
            if (issueForThisPlan.compareTo(BigDecimal.ZERO) > 0) {
                issuedPlanIds.add(plan.getId());
            }
        }
        if (issuedPlanIds.isEmpty()) {
            throw new ServiceException("下发失败,无可下发数量");
        }
        // 生成生产订单主单,并绑定本次下发关联的计划
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setProductionPlanIds(formatPlanIds(planIds));
        productionOrder.setProductionPlanIds(formatPlanIds(issuedPlanIds));
        productionOrder.setProductModelId(firstPlan.getProductModelId());
        productionOrder.setQuantity(assignedQuantity);
        productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
        boolean saved = productionOrderService.saveProductionOrder(productionOrder);
        if (!saved) {
        if (!productionOrderService.saveProductionOrder(productionOrder)) {
            throw new ServiceException("下发失败,生产订单保存失败");
        }
        int targetStatus = assignedQuantity.compareTo(totalRequiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED;
        List<ProductionPlan> updates = planIds.stream().map(id -> {
        // 回写每条计划的累计下发量和状态
        BigDecimal remainingAssignedQuantity = assignedQuantity;
        List<ProductionPlan> updates = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
            if (requiredQuantity.compareTo(BigDecimal.ZERO) < 0) {
                requiredQuantity = BigDecimal.ZERO;
            }
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            BigDecimal historicalIssuedQuantity = requiredQuantity.subtract(remainingQuantity);
            BigDecimal issuedQuantity = BigDecimal.ZERO;
            if (remainingAssignedQuantity.compareTo(BigDecimal.ZERO) > 0 && remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
                issuedQuantity = remainingAssignedQuantity.min(remainingQuantity);
                remainingAssignedQuantity = remainingAssignedQuantity.subtract(issuedQuantity);
            }
            BigDecimal totalIssuedQuantity = historicalIssuedQuantity.add(issuedQuantity);
            int planStatus = resolvePlanStatus(requiredQuantity, totalIssuedQuantity);
            ProductionPlan update = new ProductionPlan();
            update.setId(id);
            update.setStatus(targetStatus);
            return update;
        }).collect(Collectors.toList());
            update.setId(plan.getId());
            update.setStatus(planStatus);
            update.setQuantityIssued(totalIssuedQuantity);
            updates.add(update);
        }
        if (!updates.isEmpty()) {
            // 批量更新计划状态与数量
            this.updateBatchById(updates);
        }
        return true;
@@ -138,17 +189,24 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ProductionPlanDto dto) {
        // 新增主生产计划
        if (StringUtils.isBlank(dto.getMpsNo())) {
            dto.setMpsNo(generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        }else checkMpsNoUnique(dto.getMpsNo(), null);
            String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
            dto.setMpsNo(buildPlanNo(datePrefix, resolveNextPlanSequence(datePrefix)));
        } else {
            checkMpsNoUnique(dto.getMpsNo(), null);
        }
        dto.setStatus(PLAN_STATUS_WAIT);
        dto.setSource("内部");
        if (StringUtils.isBlank(dto.getSource())) {
            dto.setSource("内部");
        }
        return productionPlanMapper.insert(dto) > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean update(ProductionPlanDto dto) {
        // 更新主生产计划
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("编辑失败,数据不能为空");
        }
@@ -169,20 +227,10 @@
        return productionPlanMapper.updateById(dto) > 0;
    }
    private void checkMpsNoUnique(String mpsNo, Long excludeId) {
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getMpsNo, mpsNo);
        if (excludeId != null) {
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            throw new ServiceException("生产计划号 " + mpsNo + " 已存在");
        }
    }
    @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() == PLAN_STATUS_PARTIAL || p.getStatus() == PLAN_STATUS_ISSUED)) {
@@ -201,6 +249,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importProdData(MultipartFile file) {
        // 参数与前置条件校验
        if (file == null || file.isEmpty()) {
            throw new ServiceException("导入数据不能为空");
        }
@@ -214,76 +263,251 @@
        if (list == null || list.isEmpty()) {
            throw new ServiceException("Excel没有数据");
        }
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        int nextSequence = resolveNextPlanSequence(datePrefix);
        Set<String> mpsNos = new HashSet<>();
        // 遍历处理数据并组装结果
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            String mpsNo = dto.getMpsNo();
            String mpsNo = StringUtils.trim(dto.getMpsNo());
            if (StringUtils.isEmpty(mpsNo)) {
                generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
                mpsNo = buildPlanNo(datePrefix, nextSequence++);
            }
            dto.setMpsNo(mpsNo);
            if (!mpsNos.add(mpsNo)) {
                throw new ServiceException("导入失败:Excel 中存在重复的申请单编号 " + mpsNo);
                throw new ServiceException("导入失败,Excel中存在重复的主生产计划号 " + mpsNo);
            }
            if (dto.getQtyRequired() == null || dto.getQtyRequired().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("导入失败:第" + (i + 2) + "行需求数量必须大于0");
            }
        }
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery()
                .in(ProductionPlan::getMpsNo, mpsNos));
        // 查询并准备业务数据
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos));
        if (existApplyNoCount > 0) {
            List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery()
                            .in(ProductionPlan::getMpsNo, mpsNos))
            List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos))
                    .stream()
                    .map(ProductionPlan::getMpsNo)
                    .collect(Collectors.toList());
            throw new ServiceException("导入失败,生产计划号已存在: " + String.join(", ", existMpsNos));
            throw new ServiceException("导入失败,主生产计划号已存在: " + String.join(", ", existMpsNos));
        }
        List<ProductModel> allModels = productModelMapper.selectList(Wrappers.<ProductModel>lambdaQuery());
        Set<Long> productIds = allModels.stream()
                .map(ProductModel::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> productNameById = productIds.isEmpty()
                ? Collections.emptyMap()
                : productMapper.selectBatchIds(productIds).stream()
                .collect(Collectors.toMap(Product::getId, Product::getProductName, (a, b) -> a));
        LocalDateTime now = LocalDateTime.now();
        List<ProductionPlan> entityList = list.stream().map(dto -> {
        List<ProductionPlan> entityList = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            ProductionPlan entity = new ProductionPlan();
            BeanUtils.copyProperties(dto, entity);
            entity.setProductModelId(resolveProductModelId(dto, i + 2, allModels, productNameById));
            entity.setStatus(PLAN_STATUS_WAIT);
            entity.setSource("内部");
            entity.setSource(StringUtils.isNotEmpty(dto.getSource()) ? StringUtils.trim(dto.getSource()) : "内部");
            entity.setQuantityIssued(BigDecimal.ZERO);
            entity.setCreateTime(now);
            entity.setUpdateTime(now);
            return entity;
        }).collect(Collectors.toList());
        this.saveBatch(entityList);
            entityList.add(entity);
        }
        // 持久化或输出处理结果
        if (!this.saveBatch(entityList)) {
            throw new ServiceException("导入失败,保存生产计划数据失败");
        }
    }
    @Override
    public void exportProdData(HttpServletResponse response, List<Long> ids) {
        List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids);
    public void exportProdData(HttpServletResponse response, ProductionPlanDto requestDto) {
        // 导出主生产计划数据
        List<Long> ids = requestDto == null || requestDto.getIds() == null
                ? Collections.emptyList()
                : requestDto.getIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<ProductionPlanImportDto> exportList = new ArrayList<>();
        for (ProductionPlanDto entity : list) {
            ProductionPlanImportDto dto = new ProductionPlanImportDto();
            BeanUtils.copyProperties(entity, dto);
            exportList.add(dto);
        if (!ids.isEmpty()) {
            List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids);
            for (ProductionPlanDto item : list) {
                ProductionPlanImportDto dto = new ProductionPlanImportDto();
                BeanUtils.copyProperties(item, dto);
                dto.setAssignedQuantity(item.getQuantityIssued());
                exportList.add(dto);
            }
        } else {
            ProductionPlanDto query = new ProductionPlanDto();
            if (requestDto != null) {
                BeanUtils.copyProperties(requestDto, query);
            }
            IPage<ProductionPlanVo> page = productionPlanMapper.listPage(new Page<>(1, -1), query);
            if (page != null && page.getRecords() != null) {
                for (ProductionPlanVo item : page.getRecords()) {
                    ProductionPlanImportDto dto = new ProductionPlanImportDto();
                    BeanUtils.copyProperties(item, dto);
                    dto.setAssignedQuantity(item.getQuantityIssued());
                    exportList.add(dto);
                }
            }
        }
        ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class);
        util.exportExcel(response, exportList, "主生产计划");
    }
    private String formatPlanIds(List<Long> planIds) {
        return planIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    /**
     * 校验主生产计划号唯一性,可通过 excludeId 排除当前记录。
     */
    private void checkMpsNoUnique(String mpsNo, Long excludeId) {
        // 按主生产计划号查询重复记录
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getMpsNo, mpsNo);
        if (excludeId != null) {
            // 更新时排除当前记录本身
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            // 存在重复计划号,直接拦截
            throw new ServiceException("生产计划号 " + mpsNo + " 已存在");
        }
    }
    private String generateNextPlanNo(String datePrefix) {
    /**
     * 根据导入行的型号、产品名称、单位定位唯一的产品型号 ID。
     */
    private Long resolveProductModelId(ProductionPlanImportDto dto, int rowNo, List<ProductModel> allModels,
                                       Map<Long, String> productNameById) {
        // 先按规格型号做第一轮过滤
        String model = StringUtils.trim(dto.getModel());
        if (StringUtils.isEmpty(model)) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号不能为空");
        }
        List<ProductModel> candidates = allModels.stream()
                .filter(item -> model.equals(StringUtils.trim(item.getModel())))
                .collect(Collectors.toList());
        if (candidates.isEmpty()) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号不存在,型号:" + model);
        }
        // 若传了产品名称,再做第二轮过滤
        String productName = StringUtils.trim(dto.getProductName());
        if (StringUtils.isNotEmpty(productName)) {
            candidates = candidates.stream()
                    .filter(item -> productName.equals(StringUtils.trim(productNameById.get(item.getProductId()))))
                    .collect(Collectors.toList());
            if (candidates.isEmpty()) {
                throw new ServiceException("导入失败:第" + rowNo + "行产品名称与规格型号不匹配");
            }
        }
        // 若传了单位,再做第三轮过滤
        String unit = StringUtils.trim(dto.getUnit());
        if (StringUtils.isNotEmpty(unit)) {
            candidates = candidates.stream()
                    .filter(item -> unit.equals(StringUtils.trim(item.getUnit())))
                    .collect(Collectors.toList());
            if (candidates.isEmpty()) {
                throw new ServiceException("导入失败:第" + rowNo + "行单位与规格型号不匹配");
            }
        }
        // 仍然多条说明信息不足以唯一定位
        if (candidates.size() > 1) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号匹配到多个产品,请补充产品名称或单位");
        }
        return candidates.get(0).getId();
    }
    /**
     * 生成主生产计划号,格式:JH + yyyyMMdd + 4位流水号。
     */
    private String buildPlanNo(String datePrefix, int sequence) {
        // 统一计划号格式:JH + 日期 + 4位流水号
        return "JH" + datePrefix + String.format("%04d", sequence);
    }
    /**
     * 查询当日已存在的最大流水号,并返回下一个可用流水号。
     */
    private int resolveNextPlanSequence(String datePrefix) {
        // 查询当日最新一条计划号
        QueryWrapper<ProductionPlan> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("mps_no", "JH" + datePrefix);
        queryWrapper.orderByDesc("mps_no");
        queryWrapper.last("LIMIT 1");
        ProductionPlan latestPlan = productionPlanMapper.selectOne(queryWrapper);
        // 默认从 0001 开始
        int sequence = 1;
        if (latestPlan != null && latestPlan.getMpsNo() != null && !latestPlan.getMpsNo().isEmpty()) {
        if (latestPlan != null && StringUtils.isNotEmpty(latestPlan.getMpsNo())) {
            // 截取末尾流水号并递增
            String sequenceStr = latestPlan.getMpsNo().substring(("JH" + datePrefix).length());
            try {
                sequence = Integer.parseInt(sequenceStr) + 1;
            } catch (NumberFormatException e) {
            } catch (NumberFormatException ignored) {
                // 历史数据格式异常时回退到 0001
                sequence = 1;
            }
        }
        return "JH" + datePrefix + String.format("%04d", sequence);
        return sequence;
    }
    /**
     * 计算生产计划的剩余未下发数量(需求量 - 已下发量,最小为 0)。
     */
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        // 空对象按 0 处理
        if (plan == null) {
            return BigDecimal.ZERO;
        }
        // 需求量为空或小于等于 0,视为无剩余
        BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
        if (requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // 已下发量为空或小于等于 0,剩余即需求量
        BigDecimal issuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
        if (issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return requiredQuantity;
        }
        // 已下发量大于等于需求量,剩余归零
        if (issuedQuantity.compareTo(requiredQuantity) >= 0) {
            return BigDecimal.ZERO;
        }
        // 正常场景返回差值
        return requiredQuantity.subtract(issuedQuantity);
    }
    /**
     * 按需求量与累计下发量推导计划状态。
     */
    private int resolvePlanStatus(BigDecimal requiredQuantity, BigDecimal issuedQuantity) {
        // 无有效需求量时,状态保持待下发
        if (requiredQuantity == null || requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
        }
        // 有需求但未下发,状态仍为待下发
        if (issuedQuantity == null || issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
        }
        // 已下发量小于需求量为部分下发,否则为已下发
        return issuedQuantity.compareTo(requiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED;
    }
    /**
     * 将计划 ID 集合转成 [1,2,3] 形式,写入生产订单关联字段。
     */
    private String formatPlanIds(List<Long> planIds) {
        // 去重并拼接为 [1,2,3] 形式的字符串
        return planIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    }
}