7 小时以前 7bf754a7835d06f26240c4ca15bc5f83650de377
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -4,28 +4,46 @@
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.StockQualifiedRecordTypeEnum;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.service.impl.TempFileServiceImpl;
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.pojo.SalesLedgerProduct;
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.ICommonFileService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
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
@@ -33,75 +51,256 @@
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class ShippingInfoServiceImpl extends ServiceImpl<ShippingInfoMapper, ShippingInfo> implements ShippingInfoService {
    @Autowired
    private ShippingInfoMapper shippingInfoMapper;
    /**
     * listPage 的行来源标记:shipping=发货单,oilOut=销售类油品出库
     */
    private static final String SOURCE_OIL_OUT = "oilOut";
    @Autowired
    private TempFileServiceImpl tempFileService;
    @Autowired
    private SalesLedgerProductMapper salesLedgerProductMapper;
    @Autowired
    private StockUtils stockUtils;
    @Autowired
    private CommonFileServiceImpl commonFileService;
    private final ShippingInfoMapper shippingInfoMapper;
    @Autowired
    private ApproveProcessServiceImpl approveProcessService;
    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<ShippingInfo> listPage(Page page, ShippingInfo req) {
        IPage<ShippingInfo> listPage = shippingInfoMapper.listPage(page, req);
    public IPage<ShippingInfoDto> listPage(Page page, ShippingInfo req) {
        IPage<ShippingInfoDto> 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;
    }
    /**
     * 发货台账层级化:父行 = 一次发货(发货单 / 油品出库单),子行 = 该出库单下的到货记录。
     * <p>
     * 刻意不把到货记录 UNION 进分页 SQL:现有 UNION 的每一行天然就是一个父行,父行维度的分页与
     * total 已经正确;混入子行会撑大条数,且 {@code getDateilByShippingNo} 复用同一 listPage 取
     * 第一行时会静默取错。改成从已分页的父行里收集 id,一次批量查子行,避免 N+1。
     */
    private void assembleArrivals(List<ShippingInfoDto> records) {
        List<Long> stockOutRecordIds = records.stream()
                .filter(item -> SOURCE_OIL_OUT.equals(item.getSource()) && item.getStockOutRecordId() != null)
                .map(ShippingInfoDto::getStockOutRecordId)
                .toList();
        if (stockOutRecordIds.isEmpty()) {
            return;
        }
        Map<Long, List<SalesLedgerArrivalDto>> 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<SalesLedgerArrivalDto> arrivals = arrivalMap.get(item.getStockOutRecordId());
            item.setChildren(arrivals == null ? new ArrayList<>() : new ArrayList<>(arrivals));
        });
    }
    /**
     * 油品出库行的批号明细,供发货台账详情弹框的「批号」明细表展示
     */
    private List<ShippingProductDetailDto> 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) throws IOException {
    public boolean deductStock(ShippingInfoDto req) {
        ShippingInfo byId = this.getById(req.getId());
        if (byId == null) {
            throw new RuntimeException("发货信息不存在");
        }
        //扣减库存
        if (!"已发货".equals(byId.getStatus())) {
            List<ShippingProductDetail> shippingProductDetails = shippingProductDetailMapper.selectList(new LambdaQueryWrapper<ShippingProductDetail>().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());
        boolean update = this.updateById(req);
        //扣减库存
        SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(req.getSalesLedgerProductId());
        stockUtils.substractStock(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId());
        // 迁移文件
        if(CollectionUtils.isNotEmpty(req.getTempFileIds())){
            tempFileService.migrateTempFilesToFormal(req.getId(), req.getTempFileIds(), FileNameType.SHIP.getValue());
        }
        return update ;
        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<Long> ids) {
        List<ShippingInfo> shippingInfos = shippingInfoMapper.selectList(new LambdaQueryWrapper<ShippingInfo>()
                .in(ShippingInfo::getId, ids));
        if(CollectionUtils.isEmpty(shippingInfos)) return false;
        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());
        // 扣已发货库存
        for (ShippingInfo shippingInfo : shippingInfos) {
            if("已发货".equals(shippingInfo.getStatus())) {
                stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
        // 删除发货审批
        if (CollectionUtils.isNotEmpty(shippingInfos)) {
            for (ShippingInfo shippingInfo : shippingInfos) {
                List<ApproveProcess> one = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
                        .like(ApproveProcess::getApproveReason, shippingInfo.getShippingNo()));
                if (one != null) {
                    List<Long> list = one.stream().map(ApproveProcess::getId).toList();
                    approveProcessService.delByIds(list);
                }
                // 扣已发货库存
                stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            }
        }
        // 删除发货审批
        if(CollectionUtils.isNotEmpty(shippingInfos)){
            for (ShippingInfo shippingInfo : shippingInfos){
                ApproveProcess one = approveProcessService.getOne(new LambdaQueryWrapper<ApproveProcess>()
                        .like(ApproveProcess::getApproveReason, shippingInfo.getShippingNo()));
                if(one != null){
                    approveProcessService.delByIds(Collections.singletonList(one.getId()));
                }
        //删除发货明细
        shippingProductDetailMapper.delete(new LambdaQueryWrapper<ShippingProductDetail>().in(ShippingProductDetail::getShippingInfoId, ids));
        return this.removeByIds(ids);
    }
    @Override
    public List<ShippingProductVo> getReturnManagementDtoById(Long shippingId) {
        return shippingInfoMapper.getReturnManagementDtoById(shippingId);
    }
    @Override
    public List<ShippingInfo> 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<ShippingProductDetailDto> 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<ShippingProductDetailDto> 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<ShippingInfo>()
                    .eq(ShippingInfo::getSalesLedgerProductId, salesLedgerProductId)
                    .in(ShippingInfo::getStatus, "待审核", "审核中"));
            if (pendingCount > 0) {
                throw new RuntimeException("该产品已有待审核的发货记录,请勿重复提交");
            }
        }
        return this.removeBatchByIds(ids);
        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<ApprovalTemplate>()
                        .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<ShippingProductDetail>()
                .eq(ShippingProductDetail::getShippingInfoId, shippingInfoId));
        this.removeById(shippingInfoId);
    }
}