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