gaoluyang
3 天以前 d39e4f7d839167c16425086dfa2f8747bccfdff9
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. 创建数据权限(负责人)
        // 6. 记录流转
        String createContent = "创建售后工单【" + no + "】,处理人:" + getHandlerName(createReqVO.getHandlerUserId());
        ticketFlowService.record(ticket.getId(), AfterSaleTicketFlowService.ACTION_CREATE, "创建工单",
                null, ticket.getStatus(), createContent);
        // 7. 创建数据权限(负责人)
        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) {