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.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.production.bean.dto.ProductionPlanDto; import com.ruoyi.production.bean.dto.ProductionOrderDto; import com.ruoyi.production.bean.vo.ProductionPlanVo; 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.List; import java.util.Objects; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(rollbackFor = Exception.class) public class ProductionPlanServiceImpl extends ServiceImpl implements ProductionPlanService { private final ProductionOrderService productionOrderService; private final SalesLedgerMapper salesLedgerMapper; private final SalesLedgerProductMapper salesLedgerProductMapper; @Override public IPage listPage(Page page, ProductionPlanDto productionPlanDto) { Page entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); return this.page(entityPage, buildQueryWrapper(productionPlanDto)) .convert(item -> BeanUtil.copyProperties(item, ProductionPlanVo.class)); } @Override public void loadProdData() { // 用销售明细作为来源同步生产计划,source 字段承担幂等去重作用。 List salesLedgerProducts = salesLedgerProductMapper.selectList( Wrappers.lambdaQuery() .isNotNull(SalesLedgerProduct::getProductModelId) .gt(SalesLedgerProduct::getQuantity, BigDecimal.ZERO)); for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) { String source = buildSalesSource(salesLedgerProduct.getId()); long exists = this.count(Wrappers.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 public boolean combine(ProductionPlanDto productionPlanDto) { // 多个计划合并转单后,仍统一走生产订单保存逻辑,避免两套下单流程不一致。 if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) { throw new ServiceException("请选择生产计划"); } List 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 ids) { // 删除前校验 issued,避免把已转单计划直接从源头删掉。 if (ids == null || ids.isEmpty()) { return false; } List productionPlans = this.listByIds(ids); if (productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued()))) { throw new ServiceException("已下发的生产计划不允许删除"); } return this.removeByIds(ids); } @Override public void importProdData(MultipartFile file) { } @Override public void exportProdData(HttpServletResponse response, List ids) { } private LambdaQueryWrapper buildQueryWrapper(ProductionPlanDto dto) { ProductionPlan query = dto == null ? new ProductionPlan() : dto; return Wrappers.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("生产计划不能为空"); } if (update && productionPlan.getId() == null) { throw new ServiceException("生产计划ID不能为空"); } 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.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 productionPlans) { return productionPlans.stream() .map(ProductionPlan::getId) .distinct() .map(String::valueOf) .collect(Collectors.joining(",", "[", "]")); } private LocalDate resolveEarliestPlanDate(List 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; } }