package com.ruoyi.sales.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.approve.bean.dto.ApprovalInstanceDto; import com.ruoyi.approve.mapper.ApprovalTemplateMapper; import com.ruoyi.approve.pojo.ApprovalTemplate; import com.ruoyi.approve.pojo.ApproveProcess; import com.ruoyi.approve.service.ApprovalInstanceService; import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl; import com.ruoyi.basic.enums.ApplicationTypeEnum; import com.ruoyi.basic.enums.RecordTypeEnum; import com.ruoyi.basic.utils.FileUtil; import com.ruoyi.common.enums.FileNameType; import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum; import com.ruoyi.common.utils.OrderUtils; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.security.LoginUser; import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo; import com.ruoyi.procurementrecord.utils.StockUtils; import com.ruoyi.sales.dto.SalesLedgerArrivalDto; import com.ruoyi.sales.dto.ShippingApproveDto; import com.ruoyi.sales.dto.ShippingInfoDto; import com.ruoyi.sales.dto.ShippingProductDetailDto; import com.ruoyi.sales.mapper.SalesLedgerArrivalMapper; import com.ruoyi.sales.mapper.SalesLedgerProductMapper; import com.ruoyi.sales.mapper.ShippingInfoMapper; import com.ruoyi.sales.mapper.ShippingProductDetailMapper; import com.ruoyi.sales.pojo.ShippingInfo; import com.ruoyi.sales.pojo.ShippingProductDetail; import com.ruoyi.sales.service.ShippingInfoService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * @author :yys * @date : 2025/10/22 9:33 */ @Service @Slf4j @RequiredArgsConstructor public class ShippingInfoServiceImpl extends ServiceImpl implements ShippingInfoService { /** * listPage 的行来源标记:shipping=发货单,oilOut=销售类油品出库 */ private static final String SOURCE_OIL_OUT = "oilOut"; private final ShippingInfoMapper shippingInfoMapper; private final SalesLedgerArrivalMapper salesLedgerArrivalMapper; private final SalesLedgerProductMapper salesLedgerProductMapper; private final StockUtils stockUtils; private final CommonFileServiceImpl commonFileService; private final ApproveProcessServiceImpl approveProcessService; private final FileUtil fileUtil; private final ShippingProductDetailMapper shippingProductDetailMapper; private final ApprovalTemplateMapper approvalTemplateMapper; private final ApprovalInstanceService approvalInstanceService; @Override public IPage listPage(Page page, ShippingInfo req) { IPage listPage = shippingInfoMapper.listPage(page, req); listPage.getRecords().forEach(item -> { if (SOURCE_OIL_OUT.equals(item.getSource())) { // 油品出库行的 id 是 stock_out_record.id,与 shipping_info.id 同域会撞号, // 用它去查附件只会捞出别的发货单的图,索性不查;附件挂在到货记录子行上 // 油品出库行没有 shipping_product_detail,批号明细只能由出库记录自身字段构造 item.setBatchNoList(buildOilOutBatchNoList(item)); return; } item.setStorageBlobVOs(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, item.getId())); }); assembleArrivals(listPage.getRecords()); return listPage; } /** * 发货台账层级化:父行 = 一次发货(发货单 / 油品出库单),子行 = 该出库单下的到货记录。 *

* 刻意不把到货记录 UNION 进分页 SQL:现有 UNION 的每一行天然就是一个父行,父行维度的分页与 * total 已经正确;混入子行会撑大条数,且 {@code getDateilByShippingNo} 复用同一 listPage 取 * 第一行时会静默取错。改成从已分页的父行里收集 id,一次批量查子行,避免 N+1。 */ private void assembleArrivals(List records) { List stockOutRecordIds = records.stream() .filter(item -> SOURCE_OIL_OUT.equals(item.getSource()) && item.getStockOutRecordId() != null) .map(ShippingInfoDto::getStockOutRecordId) .toList(); if (stockOutRecordIds.isEmpty()) { return; } Map> arrivalMap = salesLedgerArrivalMapper .listByStockOutRecordIds(stockOutRecordIds).stream() .collect(Collectors.groupingBy(SalesLedgerArrivalDto::getStockOutRecordId)); records.forEach(item -> { if (!SOURCE_OIL_OUT.equals(item.getSource()) || item.getStockOutRecordId() == null) { return; } List arrivals = arrivalMap.get(item.getStockOutRecordId()); item.setChildren(arrivals == null ? new ArrayList<>() : new ArrayList<>(arrivals)); }); } /** * 油品出库行的批号明细,供发货台账详情弹框的「批号」明细表展示 */ private List buildOilOutBatchNoList(ShippingInfoDto item) { ShippingProductDetailDto detail = new ShippingProductDetailDto(); detail.setBatchNo(item.getOutBatchNo()); detail.setProductName(item.getProductName()); detail.setSpecificationModel(item.getSpecificationModel()); detail.setDeliveryQuantity(item.getTotalQuantity()); return Collections.singletonList(detail); } @Override public boolean deductStock(ShippingInfoDto req) { ShippingInfo byId = this.getById(req.getId()); if (byId == null) { throw new RuntimeException("发货信息不存在"); } //扣减库存 if (!"已发货".equals(byId.getStatus())) { List shippingProductDetails = shippingProductDetailMapper.selectList(new LambdaQueryWrapper().eq(ShippingProductDetail::getShippingInfoId, req.getId())); if (CollectionUtils.isEmpty(shippingProductDetails)) { throw new RuntimeException("发货信息不存在"); } for (ShippingProductDetail shippingProductDetail : shippingProductDetails) { stockUtils.substractStock(shippingProductDetail.getProductModelId(), shippingProductDetail.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(), shippingProductDetail.getBatchNo()); } } byId.setExpressNumber(req.getExpressNumber()); byId.setExpressCompany(req.getExpressCompany()); byId.setStatus("已发货"); byId.setShippingCarNumber(req.getShippingCarNumber()); byId.setShippingDate(req.getShippingDate()); boolean update = this.updateById(byId); // 保存文件 fileUtil.saveStorageAttachment(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, req.getId(), req.getStorageBlobDTOs()); return update; } @Override @Transactional(rollbackFor = Exception.class) public boolean delete(List ids) { List shippingInfos = shippingInfoMapper.selectList(new LambdaQueryWrapper() .in(ShippingInfo::getId, ids)); if (CollectionUtils.isEmpty(shippingInfos)) return false; // 只有待审核状态才能删除 boolean allPending = shippingInfos.stream().allMatch(s -> "待审核".equals(s.getStatus())); if (!allPending) throw new RuntimeException("只有待审核状态才能删除"); // 删除附件 commonFileService.deleteByBusinessIds(ids, FileNameType.SHIP.getValue()); // 删除发货审批 if (CollectionUtils.isNotEmpty(shippingInfos)) { for (ShippingInfo shippingInfo : shippingInfos) { List one = approveProcessService.list(new LambdaQueryWrapper() .like(ApproveProcess::getApproveReason, shippingInfo.getShippingNo())); if (one != null) { List list = one.stream().map(ApproveProcess::getId).toList(); approveProcessService.delByIds(list); } // 扣已发货库存 stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode()); } } //删除发货明细 shippingProductDetailMapper.delete(new LambdaQueryWrapper().in(ShippingProductDetail::getShippingInfoId, ids)); return this.removeByIds(ids); } @Override public List getReturnManagementDtoById(Long shippingId) { return shippingInfoMapper.getReturnManagementDtoById(shippingId); } @Override public List getShippingInfoByCustomerName(String customerName) { return shippingInfoMapper.getShippingInfoByCustomerName(customerName); } @Override public boolean add(ShippingInfoDto req) { this.save(req); req.getBatchNoDetailList().forEach(item -> item.setShippingInfoId(req.getId())); shippingProductDetailMapper.insert(req.getBatchNoDetailList()); for (ShippingProductDetail shippingProductDetail : req.getBatchNoDetailList()) { stockUtils.substractStock(shippingProductDetail.getProductModelId(), shippingProductDetail.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(), shippingProductDetail.getBatchNo()); } // 保存文件 fileUtil.saveStorageAttachment(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, req.getId(), req.getStorageBlobDTOs()); return true; } @Override public List getDetail(Long id) { return shippingProductDetailMapper.getDetail(id); } @Override public ShippingApproveDto getDateilByShippingNo(String shippingNo) { ShippingApproveDto shippingApproveDto = new ShippingApproveDto(); ShippingInfo shippingInfo = new ShippingInfo(); shippingInfo.setShippingNo(shippingNo); shippingApproveDto.setShippingInfo(shippingInfoMapper.listPage(new Page(1, -1), shippingInfo).getRecords().get(0)); List dateilByShippingNo = shippingProductDetailMapper.getDateilByShippingNo(shippingNo); shippingApproveDto.setShippingProductDetailDtoList(dateilByShippingNo); return shippingApproveDto; } @Override public boolean addReq(ShippingInfoDto req) { // 校验该产品是否已有待审核/审核中的发货记录,防止重复提交 Long salesLedgerProductId = req.getSalesLedgerProductId(); if (salesLedgerProductId != null) { long pendingCount = this.count(new LambdaQueryWrapper() .eq(ShippingInfo::getSalesLedgerProductId, salesLedgerProductId) .in(ShippingInfo::getStatus, "待审核", "审核中")); if (pendingCount > 0) { throw new RuntimeException("该产品已有待审核的发货记录,请勿重复提交"); } } LoginUser loginUser = SecurityUtils.getLoginUser(); String sh = OrderUtils.countTodayByCreateTime(shippingInfoMapper, "SH","shipping_no",req.getCreateTime()); // 先保存发货单,再发起审批;无审核人自动通过时需要按发货编号回写发货状态。 req.setShippingNo(sh); req.setStatus("待审核"); boolean save = this.add(req); // 发货审批 ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectOne( new LambdaQueryWrapper() .eq(ApprovalTemplate::getBusinessType, 7L) .eq(ApprovalTemplate::getDeleted, 0) .orderByDesc(ApprovalTemplate::getId) .last("LIMIT 1") ); if (approvalTemplate == null) { throw new RuntimeException("请先配置发货审批模板"); } ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto(); approvalInstance.setTemplateId(approvalTemplate.getId()); approvalInstance.setTemplateName(approvalTemplate.getTemplateName()); approvalInstance.setBusinessId(req.getId()); approvalInstance.setBusinessType(7L); approvalInstance.setCurrentLevel(1); approvalInstance.setTitle(sh); approvalInstance.setApplicantId(loginUser.getUserId()); approvalInstance.setApplicantName(loginUser.getNickName()); approvalInstance.setApplyTime(LocalDateTime.now()); approvalInstanceService.add(approvalInstance); // 创建审批实例后,更新发货状态为"审核中",使前端正确显示审批状态 req.setStatus("审核中"); this.updateById(req); return true; } @Override @Transactional(rollbackFor = Exception.class) public Long addShippedFromOutbound(ShippingInfoDto req) { // 仓储物流出库即发货:不发起发货审批,直接落为已发货,库存由 add 内部扣减 req.setShippingNo(OrderUtils.countTodayByCreateTime( shippingInfoMapper, "SH", "shipping_no", LocalDateTime.now())); req.setStatus("已发货"); if (req.getShippingDate() == null) { req.setShippingDate(new Date()); } this.add(req); return req.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void cancelByOutbound(Long shippingInfoId) { // <=0 必须挡住:油品出库的 record_id 是 0 而 record_type 同样是「13」, // 传 0 进来会把同类型的油品出库记录当成本发货单的配对出库整批删掉 if (shippingInfoId == null || shippingInfoId <= 0) { return; } // 回滚该发货单占用的库存 stockUtils.deleteStockOutRecord(shippingInfoId, StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode()); shippingProductDetailMapper.delete(new LambdaQueryWrapper() .eq(ShippingProductDetail::getShippingInfoId, shippingInfoId)); this.removeById(shippingInfoId); } }