| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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.*; |
| | |
| | | private AfterSaleReturnItemMapper returnItemMapper; |
| | | @Resource |
| | | private AfterSaleTicketMapper ticketMapper; |
| | | @Resource |
| | | private AfterSaleTicketItemMapper ticketItemMapper; |
| | | |
| | | @Resource |
| | | private AfterSaleNoRedisDAO noRedisDAO; |
| | |
| | | } |
| | | 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()); |
| | |
| | | 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(); |
| | | } |
| | |
| | | // 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()); |
| | |
| | | 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 |
| | |
| | | @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) |
| | |
| | | @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); |
| | | } |
| | | } |
| | | |