From a4ed0f250009167453ca637ce042ef8e0b69f613 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 24 十月 2026 19:30:00 +0800
Subject: [PATCH] feat(aftersales): 增加售后退货流程管理功能

---
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java |  189 +++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 180 insertions(+), 9 deletions(-)

diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
index f165863..6db6bd9 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
+++ b/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;
@@ -31,6 +33,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 +59,8 @@
     private AfterSaleReturnItemMapper returnItemMapper;
     @Resource
     private AfterSaleTicketMapper ticketMapper;
+    @Resource
+    private AfterSaleTicketItemMapper ticketItemMapper;
 
     @Resource
     private AfterSaleNoRedisDAO noRedisDAO;
@@ -69,6 +81,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 +155,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 +180,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,6 +204,15 @@
         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. 鏇存柊閫�璐х敵璇风姸鎬�
@@ -150,6 +221,45 @@
                 .setMesReturnId(mesReturnId)
                 .setMesReturnCode(returnDO.getNo())
                 .setAuditTime(LocalDateTime.now()));
+    }
+
+    @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 +291,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 +310,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);
         }
     }
 

--
Gitblit v1.9.3