云
2026-10-25 daf9f8dbb7ba36eb283221d2c6022039aefaf97d
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
@@ -8,9 +8,11 @@
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnItemMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleReturnStatusEnum;
@@ -20,6 +22,7 @@
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -31,6 +34,14 @@
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import java.util.HashSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
@@ -49,6 +60,8 @@
    private AfterSaleReturnItemMapper returnItemMapper;
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Resource
    private AfterSaleTicketItemMapper ticketItemMapper;
    @Resource
    private AfterSaleNoRedisDAO noRedisDAO;
@@ -69,6 +82,37 @@
        }
        if (ticket.getSaleOrderId() == null) {
            throw exception(RETURN_TICKET_SALE_ORDER_REQUIRED);
        }
        if (!AfterSaleTicketStatusEnum.PROCESSING.getStatus().equals(ticket.getStatus())
                || !Objects.equals(ticket.getCustomerId(), createReqVO.getCustomerId())
                || ticket.getReturnId() != null || returnMapper.existsActiveByTicketId(ticket.getId())) {
            throw exception(RETURN_TICKET_INVALID);
        }
        if (CollUtil.isEmpty(createReqVO.getItems())) {
            throw exception(RETURN_ITEM_EMPTY);
        }
        if (createReqVO.getReturnType() == null || !List.of(1, 2, 3).contains(createReqVO.getReturnType())) {
            throw exception(RETURN_ITEM_INVALID);
        }
        Map<Long, AfterSaleTicketItemDO> ticketItems = ticketItemMapper.selectListByTicketId(ticket.getId())
                .stream().collect(Collectors.toMap(AfterSaleTicketItemDO::getId, Function.identity()));
        Set<Long> usedItems = new HashSet<>();
        for (AfterSaleReturnSaveReqVO.Item item : createReqVO.getItems()) {
            AfterSaleTicketItemDO ticketItem = ticketItems.get(item.getTicketItemId());
            if (ticketItem == null || !usedItems.add(item.getTicketItemId())
                    || item.getItemId() == null || !item.getItemId().equals(ticketItem.getItemId())
                    || (!Integer.valueOf(3).equals(createReqVO.getReturnType())
                        && !Boolean.TRUE.equals(ticketItem.getNeedReturn()))
                    || (ticketItem.getBatchId() != null && !Objects.equals(item.getBatchId(), ticketItem.getBatchId()))
                    || item.getReturnQuantity() == null || item.getReturnQuantity().signum() <= 0
                    || ticketItem.getSaleQuantity() == null
                    || item.getReturnQuantity().compareTo(ticketItem.getSaleQuantity()) > 0
                    || (ticketItem.getReturnQuantity() != null && ticketItem.getReturnQuantity().signum() > 0
                        && item.getReturnQuantity().compareTo(ticketItem.getReturnQuantity()) > 0)
                    || item.getUnitPrice() == null || item.getUnitPrice().signum() < 0
                    || item.getReturnPrice() == null || item.getReturnPrice().signum() < 0) {
                throw exception(RETURN_ITEM_INVALID);
            }
        }
        // 销售订单以工单为准,保证退货单与工单关联同一订单
        createReqVO.setSaleOrderId(ticket.getSaleOrderId());
@@ -112,9 +156,15 @@
        permissionService.createPermission(permBO);
        // 7. 更新工单状态为待退货
        ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                .setReturnId(returnDO.getId())
                .setStatus(AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus()));
        int ticketUpdated = ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getId, ticket.getId())
                .eq(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.PROCESSING.getStatus())
                .isNull(AfterSaleTicketDO::getReturnId)
                .set(AfterSaleTicketDO::getReturnId, returnDO.getId())
                .set(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus()));
        if (ticketUpdated != 1) {
            throw exception(RETURN_TICKET_INVALID);
        }
        return returnDO.getId();
    }
@@ -131,7 +181,20 @@
        // 2. 获取关联工单(用于获取销售订单号等冗余信息)
        AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
        // 3. 调用 MES 创建销售退货单
        // 3. 仅退款不产生实物入库;其余类型将明细完整带入 MES
        if (Integer.valueOf(3).equals(returnDO.getReturnType())) {
            returnMapper.updateById(new AfterSaleReturnDO().setId(id)
                    .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
                    .setAuditTime(LocalDateTime.now()));
            if (ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                    .eq(AfterSaleTicketDO::getId, returnDO.getTicketId())
                    .eq(AfterSaleTicketDO::getReturnId, id)
                    .eq(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus())
                    .set(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus())) != 1) {
                throw exception(RETURN_TICKET_INVALID);
            }
            return;
        }
        MesWmReturnSalesCreateReqDTO mesReqDTO = new MesWmReturnSalesCreateReqDTO();
        mesReqDTO.setCode(returnDO.getNo());
        mesReqDTO.setName("售后退货-" + returnDO.getNo());
@@ -142,14 +205,89 @@
        mesReqDTO.setReturnReason(returnDO.getReturnReason());
        mesReqDTO.setRemark("售后工单退货 - 工单号: " + (ticket != null ? ticket.getNo() : ""));
        mesReqDTO.setAfterSaleReturnId(returnDO.getId());
        mesReqDTO.setItems(returnItemMapper.selectListByReturnId(id).stream().map(item -> {
            MesWmReturnSalesCreateReqDTO.Item line = new MesWmReturnSalesCreateReqDTO.Item();
            line.setItemId(item.getItemId());
            line.setQuantity(item.getReturnQuantity());
            line.setBatchId(item.getBatchId());
            line.setBatchCode(item.getBatchCode());
            line.setRemark(item.getRemark());
            return line;
        }).toList());
        Long mesReturnId = mesWmReturnSalesApi.createReturnSales(mesReqDTO);
        // 4. 更新退货申请状态
        // 4. 回写 MES 解析后的批次(原单未记录批次的商品由 MES 自动生成退货批次)
        syncReturnItemBatch(id, mesReturnId);
        // 5. 更新退货申请状态
        returnMapper.updateById(new AfterSaleReturnDO().setId(id)
                .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
                .setMesReturnId(mesReturnId)
                .setMesReturnCode(returnDO.getNo())
                .setAuditTime(LocalDateTime.now()));
    }
    /**
     * 将 MES 退货单行上的批次回写到售后明细,便于页面展示与追溯
     */
    private void syncReturnItemBatch(Long returnId, Long mesReturnId) {
        List<MesWmReturnSalesLineRespDTO> mesLines = mesWmReturnSalesApi.getReturnSalesLineList(mesReturnId);
        if (CollUtil.isEmpty(mesLines)) {
            return;
        }
        Map<Long, MesWmReturnSalesLineRespDTO> lineMap = mesLines.stream()
                .filter(line -> line.getBatchId() != null)
                .collect(Collectors.toMap(MesWmReturnSalesLineRespDTO::getItemId, line -> line, (a, b) -> a));
        if (lineMap.isEmpty()) {
            return;
        }
        for (AfterSaleReturnItemDO item : returnItemMapper.selectListByReturnId(returnId)) {
            MesWmReturnSalesLineRespDTO line = lineMap.get(item.getItemId());
            if (line == null || item.getBatchId() != null) {
                continue;
            }
            returnItemMapper.updateById(new AfterSaleReturnItemDO().setId(item.getId())
                    .setBatchId(line.getBatchId()).setBatchCode(line.getBatchCode()));
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelReturn(Long id) {
        AfterSaleReturnDO returnDO = validateReturnExists(id);
        if (!List.of(AfterSaleReturnStatusEnum.DRAFT.getStatus(), AfterSaleReturnStatusEnum.APPROVED.getStatus(),
                AfterSaleReturnStatusEnum.INSPECTING.getStatus(), AfterSaleReturnStatusEnum.INSPECTED.getStatus())
                .contains(returnDO.getStatus())) {
            throw exception(RETURN_STATUS_ERROR);
        }
        if (returnDO.getMesReturnId() != null) {
            mesWmReturnSalesApi.cancelReturnSales(returnDO.getMesReturnId());
        } else {
            onMesCanceled(id);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onMesCanceled(Long returnId) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        if (AfterSaleReturnStatusEnum.CANCELED.getStatus().equals(returnDO.getStatus())) {
            return;
        }
        if (!List.of(AfterSaleReturnStatusEnum.DRAFT.getStatus(), AfterSaleReturnStatusEnum.APPROVED.getStatus(),
                AfterSaleReturnStatusEnum.INSPECTING.getStatus(), AfterSaleReturnStatusEnum.INSPECTED.getStatus())
                .contains(returnDO.getStatus())) {
            throw exception(RETURN_STATUS_ERROR);
        }
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setStatus(AfterSaleReturnStatusEnum.CANCELED.getStatus()));
        if (ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getId, returnDO.getTicketId())
                .eq(AfterSaleTicketDO::getReturnId, returnId)
                .set(AfterSaleTicketDO::getReturnId, null)
                .set(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.PROCESSING.getStatus())) != 1) {
            throw exception(RETURN_TICKET_INVALID);
        }
    }
    @Override
@@ -181,6 +319,15 @@
    @Transactional(rollbackFor = Exception.class)
    public void onQualityFinished(Long returnId, String qualityResult, String reportUrl) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        if (List.of(AfterSaleReturnStatusEnum.INSPECTED.getStatus(), AfterSaleReturnStatusEnum.INBOUND.getStatus(),
                AfterSaleReturnStatusEnum.REFUNDED.getStatus(), AfterSaleReturnStatusEnum.CLOSED.getStatus())
                .contains(returnDO.getStatus())) {
            return;
        }
        if (!AfterSaleReturnStatusEnum.APPROVED.getStatus().equals(returnDO.getStatus())
                && !AfterSaleReturnStatusEnum.INSPECTING.getStatus().equals(returnDO.getStatus())) {
            throw exception(RETURN_STATUS_ERROR);
        }
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setQualityResult(qualityResult)
                .setQualityReportUrl(reportUrl)
@@ -191,25 +338,77 @@
    @Transactional(rollbackFor = Exception.class)
    public void onInboundFinished(Long returnId) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        if (List.of(AfterSaleReturnStatusEnum.INBOUND.getStatus(), AfterSaleReturnStatusEnum.REFUNDED.getStatus(),
                AfterSaleReturnStatusEnum.CLOSED.getStatus()).contains(returnDO.getStatus())) {
            return;
        }
        if (!AfterSaleReturnStatusEnum.INSPECTED.getStatus().equals(returnDO.getStatus())) {
            throw exception(RETURN_STATUS_ERROR);
        }
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setStatus(AfterSaleReturnStatusEnum.INBOUND.getStatus())
                .setReturnInboundTime(LocalDateTime.now()));
        if (ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getId, returnDO.getTicketId())
                .eq(AfterSaleTicketDO::getReturnId, returnId)
                .eq(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus())
                .set(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus())) != 1) {
            throw exception(RETURN_TICKET_INVALID);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onRefundCompleted(Long returnId, BigDecimal actualRefundPrice) {
        completeRefund(returnId, actualRefundPrice, null, false);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void confirmRefundCompleted(Long returnId, BigDecimal actualRefundPrice, String voucherNo) {
        if (StrUtil.isBlank(voucherNo)) {
            throw exception(RETURN_REFUND_VOUCHER_REQUIRED);
        }
        completeRefund(returnId, actualRefundPrice, voucherNo.trim(), true);
    }
    private void completeRefund(Long returnId, BigDecimal actualRefundPrice, String voucherNo, boolean manual) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        if (List.of(AfterSaleReturnStatusEnum.REFUNDED.getStatus(), AfterSaleReturnStatusEnum.CLOSED.getStatus())
                .contains(returnDO.getStatus())) {
            return;
        }
        boolean refundOnly = Integer.valueOf(3).equals(returnDO.getReturnType());
        if (!(refundOnly && AfterSaleReturnStatusEnum.APPROVED.getStatus().equals(returnDO.getStatus()))
                && !AfterSaleReturnStatusEnum.INBOUND.getStatus().equals(returnDO.getStatus())) {
            throw exception(RETURN_STATUS_ERROR);
        }
        if (actualRefundPrice == null || actualRefundPrice.signum() < 0) {
            throw exception(RETURN_ITEM_INVALID);
        }
        if (manual && returnDO.getTotalReturnPrice() != null
                && actualRefundPrice.compareTo(returnDO.getTotalReturnPrice()) > 0) {
            throw exception(RETURN_REFUND_EXCEEDS_TOTAL);
        }
        if (manual && !Integer.valueOf(2).equals(returnDO.getReturnType())
                && returnDO.getTotalReturnPrice() != null && returnDO.getTotalReturnPrice().signum() > 0
                && actualRefundPrice.signum() == 0) {
            throw exception(RETURN_ITEM_INVALID);
        }
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setActualRefundPrice(actualRefundPrice)
                .setRefundVoucherNo(voucherNo)
                .setStatus(AfterSaleReturnStatusEnum.REFUNDED.getStatus())
                .setRefundTime(LocalDateTime.now()));
        // 更新关联工单状态为待退款/换货完成
        AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
        if (ticket != null) {
            ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                    .setStatus(AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus()));
        // 退款或换货完成后工单才能完结
        if (ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getId, returnDO.getTicketId())
                .eq(AfterSaleTicketDO::getReturnId, returnId)
                .eq(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus())
                .set(AfterSaleTicketDO::getStatus, AfterSaleTicketStatusEnum.COMPLETED.getStatus())
                .set(AfterSaleTicketDO::getResolvedTime, LocalDateTime.now())) != 1) {
            throw exception(RETURN_TICKET_INVALID);
        }
    }