package com.ruoyi.outsourcing.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; 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.enums.ReviewStatusEnum; import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.OrderUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.outsourcing.dto.OutsourcingOrderDto; import com.ruoyi.outsourcing.enums.OutsourcingOrderStatusEnum; import com.ruoyi.outsourcing.excel.OutsourcingOrderExcelDto; import com.ruoyi.outsourcing.mapper.OutsourcingOrderMapper; import com.ruoyi.outsourcing.mapper.OutsourcingOrderProductMapper; import com.ruoyi.outsourcing.mapper.OutsourcingPartnerMapper; import com.ruoyi.outsourcing.pojo.OutsourcingOrder; import com.ruoyi.outsourcing.pojo.OutsourcingOrderProduct; import com.ruoyi.outsourcing.pojo.OutsourcingPartner; import com.ruoyi.outsourcing.service.IOutsourcingOrderService; import com.ruoyi.stock.dto.StockInventoryDto; import com.ruoyi.stock.pojo.StockOutRecord; import com.ruoyi.stock.service.StockInventoryService; import com.ruoyi.stock.service.StockOutRecordService; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 委外订单 Service 实现 */ @Service @RequiredArgsConstructor public class OutsourcingOrderServiceImpl extends ServiceImpl implements IOutsourcingOrderService { private static final String ORDER_NO_PREFIX = "WWDD"; private final OutsourcingOrderMapper outsourcingOrderMapper; private final OutsourcingOrderProductMapper outsourcingOrderProductMapper; private final OutsourcingPartnerMapper outsourcingPartnerMapper; private final ProductMapper productMapper; private final ProductModelMapper productModelMapper; private final StockInventoryService stockInventoryService; private final StockOutRecordService stockOutRecordService; @Override public List selectOrderList(OutsourcingOrderDto dto) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(dto.getOrderNo()), OutsourcingOrder::getOrderNo, dto.getOrderNo()) .eq(dto.getPartnerId() != null, OutsourcingOrder::getPartnerId, dto.getPartnerId()) .like(StringUtils.isNotBlank(dto.getPartnerName()), OutsourcingOrder::getPartnerName, dto.getPartnerName()) .eq(dto.getStatus() != null, OutsourcingOrder::getStatus, dto.getStatus()) .like(StringUtils.isNotBlank(dto.getProjectName()), OutsourcingOrder::getProjectName, dto.getProjectName()) .ge(StringUtils.isNotBlank(dto.getStartDate()), OutsourcingOrder::getApplyDate, dto.getStartDate()) .le(StringUtils.isNotBlank(dto.getEndDate()), OutsourcingOrder::getApplyDate, dto.getEndDate()) .orderByDesc(OutsourcingOrder::getId); return this.list(wrapper); } @Override public OutsourcingOrderDto getDetail(Long id) { OutsourcingOrder order = outsourcingOrderMapper.selectById(id); if (order == null) { throw new ServiceException("委外订单不存在"); } OutsourcingOrderDto dto = new OutsourcingOrderDto(); BeanUtils.copyProperties(order, dto); dto.setProductData(listProducts(id)); return dto; } @Override @Transactional(rollbackFor = Exception.class) public Long saveOrder(OutsourcingOrderDto dto) { OutsourcingOrder order = new OutsourcingOrder(); BeanUtils.copyProperties(dto, order); order.setId(null); order.setStatus(OutsourcingOrderStatusEnum.DRAFT.getCode()); order.setOrderNo(OrderUtils.countTodayByCreateTime( outsourcingOrderMapper, ORDER_NO_PREFIX, "order_no", LocalDateTime.now())); order.setConfirmTime(null); order.setCancelReason(null); fillPartnerName(order); if (order.getApplyDate() == null) { order.setApplyDate(LocalDate.now()); } order.setContractAmount(sumProducts(dto.getProductData())); this.save(order); saveProducts(order.getId(), dto.getProductData()); return order.getId(); } @Override @Transactional(rollbackFor = Exception.class) public boolean updateOrder(OutsourcingOrderDto dto) { if (dto.getId() == null) { throw new ServiceException("委外订单id不能为空"); } OutsourcingOrder existing = outsourcingOrderMapper.selectById(dto.getId()); if (existing == null) { throw new ServiceException("委外订单不存在"); } if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(existing.getStatus())) { throw new ServiceException("仅草稿状态的委外订单允许修改"); } OutsourcingOrder order = new OutsourcingOrder(); BeanUtils.copyProperties(dto, order); // 单号、状态、确认信息由系统维护,置空后 MyBatis-Plus 不会更新这些列 order.setOrderNo(null); order.setStatus(null); order.setConfirmTime(null); order.setCancelReason(null); order.setTenantId(null); order.setDeptId(null); order.setCreateUser(null); order.setCreateTime(null); order.setUpdateTime(null); fillPartnerName(order); order.setContractAmount(sumProducts(dto.getProductData())); boolean updated = this.updateById(order); if (updated) { outsourcingOrderProductMapper.delete(Wrappers.lambdaQuery() .eq(OutsourcingOrderProduct::getOutsourcingOrderId, dto.getId())); saveProducts(dto.getId(), dto.getProductData()); } return updated; } @Override @Transactional(rollbackFor = Exception.class) public boolean deleteOrders(List ids) { if (ids == null || ids.isEmpty()) { throw new ServiceException("请选择要删除的委外订单"); } List notDraftNos = this.listByIds(ids).stream() .filter(order -> !OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus())) .map(OutsourcingOrder::getOrderNo) .filter(Objects::nonNull) .collect(Collectors.toList()); if (!notDraftNos.isEmpty()) { throw new ServiceException("仅草稿状态的委外订单允许删除:" + String.join("、", notDraftNos)); } outsourcingOrderProductMapper.delete(Wrappers.lambdaQuery() .in(OutsourcingOrderProduct::getOutsourcingOrderId, ids)); return this.removeByIds(ids); } @Override @Transactional(rollbackFor = Exception.class) public boolean submit(Long id) { OutsourcingOrder order = requireOrder(id); if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus())) { throw new ServiceException("仅草稿状态的委外订单允许提交"); } if (order.getPartnerId() == null || outsourcingPartnerMapper.selectById(order.getPartnerId()) == null) { throw new ServiceException("请先选择有效合作商再提交"); } List products = listProducts(id); if (products.isEmpty()) { throw new ServiceException("委外订单没有产品明细,无法提交"); } if (order.getContractAmount() == null || order.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("委外订单金额必须大于 0 才能提交"); } // 条件更新抢占草稿状态,避免并发下重复提交 boolean submitted = this.update(Wrappers.lambdaUpdate() .eq(OutsourcingOrder::getId, id) .eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode()) .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode()) .set(OutsourcingOrder::getConfirmTime, LocalDateTime.now())); if (!submitted) { throw new ServiceException("该委外订单已提交,请勿重复操作"); } occupyScrapStock(products); return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean complete(Long id) { OutsourcingOrder order = requireOrder(id); if (!OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(order.getStatus())) { throw new ServiceException("仅已确认状态的委外订单允许完成"); } requireScrapStockOutApproved(order.getId()); boolean completed = this.update(Wrappers.lambdaUpdate() .eq(OutsourcingOrder::getId, id) .eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode()) .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.COMPLETED.getCode())); if (!completed) { throw new ServiceException("仅已确认状态的委外订单允许完成"); } return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean cancel(Long id, String cancelReason) { OutsourcingOrder order = requireOrder(id); Integer status = order.getStatus(); if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(status) && !OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) { throw new ServiceException("该委外订单已完成或已作废,不允许重复作废"); } boolean cancelled = this.update(Wrappers.lambdaUpdate() .eq(OutsourcingOrder::getId, id) .in(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode(), OutsourcingOrderStatusEnum.CONFIRMED.getCode()) .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CANCELLED.getCode()) .set(OutsourcingOrder::getCancelReason, cancelReason)); if (!cancelled) { throw new ServiceException("该委外订单已完成或已作废,不允许重复作废"); } if (OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) { // 提交时占用的报废品库存要释放:未审批的记录删掉即释放,已审批通过的按批次归还库存 stockOutRecordService.releaseByRecordIds(scrapLineIds(id), StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode()); } return true; } @Override public void exportOrders(HttpServletResponse response, OutsourcingOrderDto dto) { List orders = selectOrderList(dto); List excelList = orders.stream().map(order -> { OutsourcingOrderExcelDto excel = new OutsourcingOrderExcelDto(); BeanUtils.copyProperties(order, excel); excel.setStatusLabel(OutsourcingOrderStatusEnum.getLabelByValue(order.getStatus())); return excel; }).collect(Collectors.toList()); ExcelUtil util = new ExcelUtil<>(OutsourcingOrderExcelDto.class); util.exportExcel(response, excelList, "委外订单"); } private OutsourcingOrder requireOrder(Long id) { if (id == null) { throw new ServiceException("委外订单id不能为空"); } OutsourcingOrder order = outsourcingOrderMapper.selectById(id); if (order == null) { throw new ServiceException("委外订单不存在"); } return order; } /** * 报废品选料行的 id,作为出库记录的 record_id。 * 出库记录一条对应一行明细(一个规格+批号),与采购退货出库的记法一致 */ private List scrapLineIds(Long orderId) { return listProducts(orderId).stream() .filter(product -> Boolean.TRUE.equals(product.getIsScrap())) .map(OutsourcingOrderProduct::getId) .collect(Collectors.toList()); } /** * 报废品选料占用库存:每条选料行写一条待审批出库记录。 * *

报废品取的是合格库存(stock_inventory,出库记录 type=0)。 * 记录一落库就通过 stock_out_record 的待审批量把可用量占住 * (可用量 = qualitity − locked_quantity − 待审批出库量),因此同一批次不会被多张委外订单超选, * 但 qualitity 本身要等出库管理审批通过才真正扣减 —— 也就是「提交即占用、审批才实扣」。

*/ private void occupyScrapStock(List products) { for (OutsourcingOrderProduct product : products) { if (!Boolean.TRUE.equals(product.getIsScrap())) { continue; } if (product.getProductModelId() == null || StringUtils.isEmpty(product.getBatchNo())) { throw new ServiceException("报废品选料行缺少产品规格或批号,无法提交"); } BigDecimal quantity = product.getQuantity(); if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("报废品选料行数量必须大于 0"); } StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setRecordId(product.getId()); stockInventoryDto.setRecordType(StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode()); stockInventoryDto.setProductModelId(product.getProductModelId()); stockInventoryDto.setBatchNo(product.getBatchNo()); stockInventoryDto.setQualitity(quantity); try { stockInventoryService.addStockOutRecordOnly(stockInventoryDto); } catch (RuntimeException e) { // 合格库存不足抛的是 ServiceException,不合格那种 BaseException 也一并兜住 throw new ServiceException("报废品「" + product.getSpecificationModel() + "」批号「" + product.getBatchNo() + "」" + e.getMessage()); } } } /** * 报废品选料的出库记录必须全部审批通过才允许完成订单, * 否则会出现「委外已完成、库存没扣」的账实不符 */ private void requireScrapStockOutApproved(Long orderId) { List lineIds = scrapLineIds(orderId); if (lineIds.isEmpty()) { return; } List records = stockOutRecordService.listByRecordIds( lineIds, StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode()); if (records.size() < lineIds.size()) { throw new ServiceException("报废品选料缺少出库记录,请先作废后重新提交"); } List pendingBatches = records.stream() .filter(record -> !ReviewStatusEnum.APPROVED.getCode().equals(record.getApprovalStatus())) .map(StockOutRecord::getOutboundBatches) .filter(Objects::nonNull) .collect(Collectors.toList()); if (!pendingBatches.isEmpty()) { throw new ServiceException("报废品出库未审批通过,请先到出库管理审批出库批次:" + String.join("、", pendingBatches)); } } private List listProducts(Long orderId) { return outsourcingOrderProductMapper.selectList(Wrappers.lambdaQuery() .eq(OutsourcingOrderProduct::getOutsourcingOrderId, orderId) .orderByAsc(OutsourcingOrderProduct::getId)); } private void saveProducts(Long orderId, List products) { if (products == null || products.isEmpty()) { return; } fillProductNames(products); for (OutsourcingOrderProduct product : products) { product.setId(null); product.setOutsourcingOrderId(orderId); product.setTenantId(null); product.setDeptId(null); product.setCreateUser(null); product.setCreateTime(null); product.setUpdateTime(null); outsourcingOrderProductMapper.insert(product); } } /** * 委外金额以明细含税总价合计为准,避免前端传入金额与明细对不上导致往来应付错乱 */ private BigDecimal sumProducts(List products) { if (products == null || products.isEmpty()) { return BigDecimal.ZERO; } return products.stream() .map(OutsourcingOrderProduct::getTaxInclusiveTotalPrice) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); } /** * 补全产品大类与规格型号,便于订单直接展示产品名称 */ private void fillProductNames(List products) { Set productIds = products.stream() .map(OutsourcingOrderProduct::getProductId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Set modelIds = products.stream() .map(OutsourcingOrderProduct::getProductModelId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map productMap = new HashMap<>(); if (!productIds.isEmpty()) { for (Product product : productMapper.selectBatchIds(productIds)) { productMap.put(product.getId(), product.getProductName()); } } Map modelMap = new HashMap<>(); if (!modelIds.isEmpty()) { for (ProductModel model : productModelMapper.selectBatchIds(modelIds)) { modelMap.put(model.getId(), model.getModel()); } } for (OutsourcingOrderProduct product : products) { String productName = productMap.get(product.getProductId()); if (productName != null) { product.setProductCategory(productName); } String model = modelMap.get(product.getProductModelId()); if (model != null) { product.setSpecificationModel(model); } } } private void fillPartnerName(OutsourcingOrder order) { if (order.getPartnerId() == null) { return; } OutsourcingPartner partner = outsourcingPartnerMapper.selectById(order.getPartnerId()); if (partner == null) { throw new ServiceException("合作商不存在,请重新选择"); } order.setPartnerName(partner.getPartnerName()); } }