From d39e4f7d839167c16425086dfa2f8747bccfdff9 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 05 十一月 2026 17:39:01 +0800
Subject: [PATCH] feat: 打通销售采购财务闭环并完成累计业务优化

---
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java |  182 +++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 165 insertions(+), 17 deletions(-)

diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
index bc01293..cbf4c26 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
@@ -12,6 +12,7 @@
 import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
 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.AfterSaleIssueTypeEnum;
 import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
 import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
 import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
@@ -19,6 +20,8 @@
 import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
 import cn.iocoder.yudao.module.aftersales.service.problem.AfterSaleProblemService;
 import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -27,6 +30,7 @@
 
 import java.time.LocalDateTime;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -39,6 +43,13 @@
 @Validated
 @Slf4j
 public class AfterSaleTicketServiceImpl implements AfterSaleTicketService {
+
+    /** 瀹㈡埛璇夋眰闄勪欢璁板綍绫诲瀷 */
+    private static final String FEEDBACK_RECORD_TYPE = "aftersales_after_sale_ticket_feedback";
+    /** 澶勭悊缁撴灉闄勪欢璁板綍绫诲瀷 */
+    private static final String RESULT_RECORD_TYPE = "aftersales_after_sale_ticket_result";
+    /** 閫氱敤闄勪欢璁板綍绫诲瀷锛堝巻鍙叉暟鎹級 */
+    private static final String LEGACY_RECORD_TYPE = "aftersales_after_sale_ticket";
 
     @Resource
     private AfterSaleTicketMapper ticketMapper;
@@ -57,23 +68,34 @@
     @Resource
     private StorageAttachmentApi storageAttachmentApi;
 
+    @Resource
+    private AfterSaleTicketFlowService ticketFlowService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createTicket(AfterSaleTicketSaveReqVO createReqVO, Long userId) {
-        // 1. 鐢熸垚鍗曞彿
+        // 1. 鏍¢獙澶勭悊浜猴細鏂板鍗冲垎閰嶏紝涓嶅啀鎻愪緵鍗曠嫭鐨勫垎閰嶅鐞嗕汉鍔ㄤ綔
+        if (createReqVO.getHandlerUserId() == null) {
+            throw exception(TICKET_HANDLER_REQUIRED);
+        }
+
+        // 2. 鐢熸垚鍗曞彿
         String no = noRedisDAO.generate(AfterSaleNoRedisDAO.TICKET_NO_PREFIX);
         if (ticketMapper.selectByNo(no) != null) {
             throw exception(TICKET_NO_EXISTS);
         }
 
-        // 2. 鎻掑叆宸ュ崟
+        // 3. 鎻掑叆宸ュ崟锛氭柊澧炴椂宸叉寚瀹氬鐞嗕汉锛岀洿鎺ヨ繘鍏ュ鐞嗕腑
         AfterSaleTicketDO ticket = BeanUtils.toBean(createReqVO, AfterSaleTicketDO.class)
                 .setNo(no)
-                .setStatus(AfterSaleTicketStatusEnum.DRAFT.getStatus())
+                .setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus())
                 .setOwnerUserId(userId);
         ticketMapper.insert(ticket);
 
-        // 3. 鎻掑叆鏄庣粏琛�
+        // 4. 鎻掑叆鏄庣粏琛�
         if (CollUtil.isNotEmpty(createReqVO.getItems())) {
             createReqVO.getItems().forEach(item -> {
                 AfterSaleTicketItemDO itemDO = BeanUtils.toBean(item, AfterSaleTicketItemDO.class);
@@ -82,11 +104,20 @@
             });
         }
 
-        // 4. 缁戝畾闄勪欢
-        storageAttachmentApi.bindAttachments("file", "aftersales_after_sale_ticket",
+        // 5. 缁戝畾闄勪欢锛氬鎴疯瘔姹傞檮浠躲�佸鐞嗙粨鏋滈檮浠跺垎寮�瀛樺偍锛涘巻鍙查�氱敤闄勪欢瀛楁缁х画鍏煎
+        storageAttachmentApi.bindAttachments("file", FEEDBACK_RECORD_TYPE,
+                ticket.getId(), createReqVO.getFeedbackBlobIds());
+        storageAttachmentApi.bindAttachments("file", RESULT_RECORD_TYPE,
+                ticket.getId(), createReqVO.getResultBlobIds());
+        storageAttachmentApi.bindAttachments("file", LEGACY_RECORD_TYPE,
                 ticket.getId(), createReqVO.getBlobIds());
 
-        // 5. 鍒涘缓鏁版嵁鏉冮檺锛堣礋璐d汉锛�
+        // 6. 璁板綍娴佽浆
+        String createContent = "鍒涘缓鍞悗宸ュ崟銆�" + no + "銆戯紝澶勭悊浜猴細" + getHandlerName(createReqVO.getHandlerUserId());
+        ticketFlowService.record(ticket.getId(), AfterSaleTicketFlowService.ACTION_CREATE, "鍒涘缓宸ュ崟",
+                null, ticket.getStatus(), createContent);
+
+        // 7. 鍒涘缓鏁版嵁鏉冮檺锛堣礋璐d汉锛�
         AfterSalePermissionCreateReqBO permBO = new AfterSalePermissionCreateReqBO();
         permBO.setBizType(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType());
         permBO.setBizId(ticket.getId());
@@ -102,10 +133,16 @@
     public void updateTicket(AfterSaleTicketSaveReqVO updateReqVO) {
         // 1. 鏍¢獙瀛樺湪
         AfterSaleTicketDO ticket = validateTicketExists(updateReqVO.getId());
-        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT);
+        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
 
-        // 2. 鏇存柊宸ュ崟
+        // 2. 鏇存柊宸ュ崟锛氬巻鍙茶崏绋胯ˉ褰曞鐞嗕汉鍚庣洿鎺ヨ繘鍏ュ鐞嗕腑锛岄伩鍏嶅洜鍙栨秷鍒嗛厤鑺傜偣鑰屽崱鍦ㄨ崏绋�
         AfterSaleTicketDO updateObj = BeanUtils.toBean(updateReqVO, AfterSaleTicketDO.class);
+        Integer targetStatus = ticket.getStatus();
+        if (AfterSaleTicketStatusEnum.DRAFT.getStatus().equals(ticket.getStatus())
+                && updateReqVO.getHandlerUserId() != null) {
+            targetStatus = AfterSaleTicketStatusEnum.PROCESSING.getStatus();
+            updateObj.setStatus(targetStatus);
+        }
         ticketMapper.updateById(updateObj);
 
         // 3. 鏇存柊鏄庣粏琛岋紙鍏堝垹鍚庡锛�
@@ -118,8 +155,17 @@
             });
         }
 
-        // 4. 鏇存柊闄勪欢
-        storageAttachmentApi.updateAttachments("file", "aftersales_after_sale_ticket", updateReqVO.getId(), updateReqVO.getBlobIds());
+        // 4. 鏇存柊闄勪欢锛氬鎴疯瘔姹傘�佸鐞嗙粨鏋滃垎鍒洿鏂帮紱鍘嗗彶閫氱敤闄勪欢淇濈暀鍘熼�昏緫
+        storageAttachmentApi.updateAttachments("file", FEEDBACK_RECORD_TYPE, updateReqVO.getId(),
+                updateReqVO.getFeedbackBlobIds());
+        storageAttachmentApi.updateAttachments("file", RESULT_RECORD_TYPE, updateReqVO.getId(),
+                updateReqVO.getResultBlobIds());
+        storageAttachmentApi.updateAttachments("file", LEGACY_RECORD_TYPE, updateReqVO.getId(),
+                updateReqVO.getBlobIds());
+
+        // 5. 璁板綍娴佽浆
+        ticketFlowService.record(ticket.getId(), AfterSaleTicketFlowService.ACTION_UPDATE, "鏇存柊宸ュ崟",
+                ticket.getStatus(), targetStatus, buildUpdateFlowContent(updateReqVO, ticket.getHandlerUserId()));
     }
 
     @Override
@@ -130,7 +176,9 @@
         // 杞垹闄�
         ticketMapper.deleteById(id);
         ticketItemMapper.deleteByTicketId(id);
-        storageAttachmentApi.deleteAttachmentsByRecord("aftersales_after_sale_ticket", id);
+        storageAttachmentApi.deleteAttachmentsByRecord(FEEDBACK_RECORD_TYPE, id);
+        storageAttachmentApi.deleteAttachmentsByRecord(RESULT_RECORD_TYPE, id);
+        storageAttachmentApi.deleteAttachmentsByRecord(LEGACY_RECORD_TYPE, id);
         permissionService.deletePermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), id);
     }
 
@@ -162,11 +210,48 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void assignTicket(Long id, Long handlerUserId) {
-        AfterSaleTicketDO ticket = validateTicketExists(id);
-        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
-        ticketMapper.updateById(new AfterSaleTicketDO().setId(id).setHandlerUserId(handlerUserId)
-                .setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus()));
+    public void onWarehouseReturnCreated(Long saleOrderId, Long returnSalesId,
+                                         String returnSalesCode, String returnReason) {
+        if (saleOrderId == null) {
+            return;
+        }
+        // 1. 鎵惧埌璇ラ攢鍞鍗曚笅鏈�杩戜竴寮犳湭鍏抽棴銆佹湭鍙栨秷鐨勫伐鍗�
+        List<AfterSaleTicketDO> tickets = ticketMapper.selectListBySaleOrderId(saleOrderId);
+        AfterSaleTicketDO ticket = tickets.stream()
+                .filter(item -> !AfterSaleTicketStatusEnum.CLOSED.getStatus().equals(item.getStatus())
+                        && !AfterSaleTicketStatusEnum.CANCELED.getStatus().equals(item.getStatus()))
+                .max(Comparator.comparing(AfterSaleTicketDO::getId))
+                .orElse(null);
+        if (ticket == null) {
+            return;
+        }
+        // 2. 鍚屼竴閫�璐у崟鍙褰曚竴娆★紝閬垮厤鍥炶皟閲嶈瘯浜х敓閲嶅璁板綍
+        String returnSalesKey = returnSalesCode != null && !returnSalesCode.isBlank()
+                ? returnSalesCode : "ID=" + returnSalesId;
+        boolean recorded = ticketFlowService.getFlowList(ticket.getId()).stream()
+                .anyMatch(flow -> AfterSaleTicketFlowService.ACTION_RETURN.equals(flow.getAction())
+                        && flow.getContent() != null
+                        && flow.getContent().contains("銆�" + returnSalesKey + "銆�"));
+        if (recorded) {
+            return;
+        }
+        // 3. 鑽夌/澶勭悊涓殑宸ュ崟杩涘叆寰呴��璐э紱鍏朵粬鐘舵�佸彧鐣欑棔锛屼笉鏀圭姸鎬�
+        Integer targetStatus = ticket.getStatus();
+        if (AfterSaleTicketStatusEnum.DRAFT.getStatus().equals(targetStatus)
+                || AfterSaleTicketStatusEnum.PROCESSING.getStatus().equals(targetStatus)) {
+            targetStatus = AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus();
+            ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId()).setStatus(targetStatus));
+        }
+        // 4. 鍐欏叆娴佽浆璁板綍
+        String content = "浠撳偍鐗╂祦鍒涘缓閿�鍞��璐у崟銆�" + returnSalesKey + "銆�"
+                + (returnReason != null && !returnReason.isBlank() ? "锛岄��璐у師鍥狅細" + returnReason : "");
+        ticketFlowService.record(ticket.getId(), AfterSaleTicketFlowService.ACTION_RETURN, "浠撳偍閫�璐�",
+                ticket.getStatus(), targetStatus, content);
+    }
+
+    private String getHandlerName(Long handlerUserId) {
+        AdminUserRespDTO handler = adminUserApi.getUser(handlerUserId);
+        return handler != null ? handler.getNickname() : "鐢ㄦ埛(ID=" + handlerUserId + ")";
     }
 
     @Override
@@ -176,6 +261,12 @@
         validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
         ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                 .setIssueType(issueType).setIssueCategory(issueCategory).setSeverityLevel(severityLevel));
+        // 璁板綍娴佽浆
+        ticketFlowService.record(id, AfterSaleTicketFlowService.ACTION_CLASSIFY, "闂鍒ゅ畾",
+                ticket.getStatus(), ticket.getStatus(),
+                "闂鍒ゅ畾锛氱被鍨�=" + getIssueTypeName(issueType)
+                        + "锛屽垎绫�=" + (issueCategory != null && !issueCategory.isBlank() ? issueCategory : "鏈~鍐�")
+                        + "锛屼弗閲嶇▼搴�=" + getSeverityName(severityLevel));
     }
 
     @Override
@@ -189,6 +280,10 @@
                 .setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
                 .setResolvedTime(LocalDateTime.now())
                 .setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus()));
+        // 璁板綍娴佽浆
+        ticketFlowService.record(id, AfterSaleTicketFlowService.ACTION_RESOLVE, "瀹岀粨宸ュ崟",
+                ticket.getStatus(), AfterSaleTicketStatusEnum.COMPLETED.getStatus(),
+                "宸ュ崟澶勭悊瀹屾垚锛岃繘鍏ュ緟鍥炶");
     }
 
     @Override
@@ -209,6 +304,10 @@
                 .setClosedTime(LocalDateTime.now())
                 .setProblemId(problem.getId())
                 .setProblemNo(problem.getNo()));
+        // 璁板綍娴佽浆
+        ticketFlowService.record(id, AfterSaleTicketFlowService.ACTION_ARCHIVE, "褰掓。宸ュ崟",
+                AfterSaleTicketStatusEnum.COMPLETED.getStatus(), AfterSaleTicketStatusEnum.CLOSED.getStatus(),
+                "褰掓。宸ュ崟锛屾矇娣�闂銆�" + problem.getNo() + "銆�");
     }
 
     @Override
@@ -218,10 +317,59 @@
         validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
         ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                 .setStatus(AfterSaleTicketStatusEnum.CANCELED.getStatus()));
+        // 璁板綍娴佽浆
+        ticketFlowService.record(id, AfterSaleTicketFlowService.ACTION_CANCEL, "鍙栨秷宸ュ崟",
+                ticket.getStatus(), AfterSaleTicketStatusEnum.CANCELED.getStatus(), "鍙栨秷宸ュ崟");
     }
 
     // ========== 鍐呴儴鏂规硶 ==========
 
+    private String buildUpdateFlowContent(AfterSaleTicketSaveReqVO reqVO, Long originalHandlerUserId) {
+        StringBuilder content = new StringBuilder("鏇存柊宸ュ崟淇℃伅");
+        if (reqVO.getHandlerUserId() != null
+                && !reqVO.getHandlerUserId().equals(originalHandlerUserId)) {
+            content.append("锛屽鐞嗕汉鍙樻洿涓猴細").append(getHandlerName(reqVO.getHandlerUserId()));
+        }
+        if (reqVO.getFeedbackDescription() != null && !reqVO.getFeedbackDescription().isBlank()) {
+            content.append("锛屽鎴疯瘔姹傚弽棣堝凡鏇存柊");
+        }
+        if (CollUtil.isNotEmpty(reqVO.getFeedbackBlobIds())) {
+            content.append("锛屽鎴疯瘔姹傞檮浠� ").append(reqVO.getFeedbackBlobIds().size()).append(" 涓�");
+        }
+        if (reqVO.getResultDescription() != null && !reqVO.getResultDescription().isBlank()) {
+            content.append("锛屽鐞嗙粨鏋滃凡鏇存柊");
+        }
+        if (CollUtil.isNotEmpty(reqVO.getResultBlobIds())) {
+            content.append("锛屽鐞嗙粨鏋滈檮浠� ").append(reqVO.getResultBlobIds().size()).append(" 涓�");
+        }
+        return content.toString();
+    }
+
+    private String getIssueTypeName(Integer issueType) {
+        if (issueType == null) {
+            return AfterSaleIssueTypeEnum.UNCLASSIFIED.getName();
+        }
+        for (AfterSaleIssueTypeEnum item : AfterSaleIssueTypeEnum.values()) {
+            if (item.getType().equals(issueType)) {
+                return item.getName();
+            }
+        }
+        return "鏈煡绫诲瀷(ID=" + issueType + ")";
+    }
+
+    private String getSeverityName(Integer severityLevel) {
+        if (severityLevel == null) {
+            return "鏈~鍐�";
+        }
+        return switch (severityLevel) {
+            case 0 -> "杞诲井";
+            case 1 -> "涓�鑸�";
+            case 2 -> "涓ラ噸";
+            case 3 -> "鑷村懡";
+            default -> "鏈煡(ID=" + severityLevel + ")";
+        };
+    }
+
     private AfterSaleTicketDO validateTicketExists(Long id) {
         AfterSaleTicketDO ticket = ticketMapper.selectById(id);
         if (ticket == null) {

--
Gitblit v1.9.3