From 46287520465b5664a1ddb0256537857cfd1cfc15 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 14 九月 2026 17:52:04 +0800
Subject: [PATCH] feat(aftersales): 售后新增回访、问题归档与问题库

---
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java                    |   95 +++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java                    |  106 +++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitTypeEnum.java                               |   44 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRootCauseCategoryEnum.java                       |   49 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java                     |    3 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java        |  141 +++++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java    |   18 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java |    3 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java      |   45 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java                |  211 +++++++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java            |   96 +++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java                    |   26 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java                      |   67 ++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java                           |   43 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java                  |    9 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java          |   27 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java                  |  118 ++++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java        |   41 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java                             |   44 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java                                |    2 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java             |   56 ++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java      |   71 ++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java         |  104 +++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java |    3 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java                            |    2 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java                   |  101 +++
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java                       |   33 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java                                   |   20 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java                        |   52 +
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java                           |    5 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java                  |   24 
 yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java                                   |   13 
 32 files changed, 1,665 insertions(+), 7 deletions(-)

diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java
new file mode 100644
index 0000000..af999db
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java
@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.problem;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
+import cn.iocoder.yudao.module.aftersales.service.problem.AfterSaleProblemService;
+import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
+import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - 鍞悗闂搴�")
+@RestController
+@RequestMapping("/aftersales/problem")
+@Validated
+public class AfterSaleProblemController {
+
+    @Resource
+    private AfterSaleProblemService problemService;
+
+    @Resource
+    private CrmCustomerApi customerApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private StorageAttachmentApi storageAttachmentApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "鍒涘缓鍞悗闂")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:create')")
+    public CommonResult<Long> createProblem(@Valid @RequestBody AfterSaleProblemSaveReqVO createReqVO) {
+        return success(problemService.createProblem(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "鏇存柊鍞悗闂")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:update')")
+    public CommonResult<Boolean> updateProblem(@Valid @RequestBody AfterSaleProblemSaveReqVO updateReqVO) {
+        problemService.updateProblem(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "鍒犻櫎鍞悗闂")
+    @Parameter(name = "id", description = "闂缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:delete')")
+    public CommonResult<Boolean> deleteProblem(@RequestParam("id") Long id) {
+        problemService.deleteProblem(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "鑾峰緱鍞悗闂")
+    @Parameter(name = "id", description = "闂缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
+    public CommonResult<AfterSaleProblemRespVO> getProblem(@RequestParam("id") Long id) {
+        AfterSaleProblemDO problemDO = problemService.getProblem(id);
+        AfterSaleProblemRespVO respVO = buildProblemDetailList(List.of(problemDO)).get(0);
+        respVO.setAttachmentList(storageAttachmentApi.listAttachments("aftersales_after_sale_problem", id));
+        return success(respVO);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "鑾峰緱鍞悗闂鍒嗛〉")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
+    public CommonResult<PageResult<AfterSaleProblemRespVO>> getProblemPage(@Valid AfterSaleProblemPageReqVO pageReqVO) {
+        PageResult<AfterSaleProblemDO> pageResult = problemService.getProblemPage(pageReqVO);
+        return success(new PageResult<>(buildProblemDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    @PutMapping("/publish")
+    @Operation(summary = "鍙戝竷鍞悗闂")
+    @Parameter(name = "id", description = "闂缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:publish')")
+    public CommonResult<Boolean> publishProblem(@RequestParam("id") Long id) {
+        problemService.publishProblem(id);
+        return success(true);
+    }
+
+    @PutMapping("/disable")
+    @Operation(summary = "鍋滅敤鍞悗闂")
+    @Parameter(name = "id", description = "闂缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:disable')")
+    public CommonResult<Boolean> disableProblem(@RequestParam("id") Long id) {
+        problemService.disableProblem(id);
+        return success(true);
+    }
+
+    @GetMapping("/reference")
+    @Operation(summary = "鏂板伐鍗曞彈鐞嗗弬鑰冩绱�(宸插彂甯冮棶棰�)")
+    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
+    public CommonResult<List<AfterSaleProblemRespVO>> getReferenceList(@Valid AfterSaleProblemPageReqVO pageReqVO) {
+        return success(buildProblemDetailList(problemService.getReferenceList(pageReqVO)));
+    }
+
+    // ========== 鍐呴儴鏂规硶锛氭壒閲忔嫾瑁呭叧鑱斿悕绉� ==========
+
+    private List<AfterSaleProblemRespVO> buildProblemDetailList(List<AfterSaleProblemDO> problemList) {
+        if (CollUtil.isEmpty(problemList)) {
+            return Collections.emptyList();
+        }
+        // 1. 鎵归噺鍔犺浇瀹㈡埛鍚嶇О
+        Set<Long> customerIds = convertSet(problemList, AfterSaleProblemDO::getCustomerId);
+        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
+        // 2. 鎵归噺鍔犺浇娌夋穩浜哄悕绉�
+        Set<Long> userIds = convertSet(problemList, AfterSaleProblemDO::getOwnerUserId);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        // 3. 鎷艰鍚嶇О瀛楁
+        return BeanUtils.toBean(problemList, AfterSaleProblemRespVO.class, vo -> {
+            CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
+            if (customer != null) {
+                vo.setCustomerName(customer.getName());
+            }
+            AdminUserRespDTO ownerUser = userMap.get(vo.getOwnerUserId());
+            if (ownerUser != null) {
+                vo.setOwnerUserName(ownerUser.getNickname());
+            }
+        });
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java
new file mode 100644
index 0000000..55fc93a
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 鍞悗闂搴� - 鍒嗛〉鏌ヨ Request VO
+ */
+@Schema(description = "鍞悗闂搴� - 鍒嗛〉鏌ヨ Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AfterSaleProblemPageReqVO extends PageParam {
+
+    @Schema(description = "闂鍗曞彿", example = "AP20260813000001")
+    private String no;
+
+    @Schema(description = "鍏抽敭瀛�(鍗曞彿/鏍囬/鎻忚堪/瑙e喅鏂规)", example = "浼犳劅鍣�")
+    private String keyword;
+
+    @Schema(description = "鏉ユ簮宸ュ崟缂栧彿", example = "1")
+    private Long sourceTicketId;
+
+    @Schema(description = "鐗╂枡缂栧彿", example = "1")
+    private Long itemId;
+
+    @Schema(description = "鐗╂枡缂栫爜(妯$硦)", example = "MAT-001")
+    private String itemCode;
+
+    @Schema(description = "闂鍒嗙被", example = "鍔熻兘鏁呴殰")
+    private String issueCategory;
+
+    @Schema(description = "闂绫诲瀷", example = "1")
+    private Integer issueType;
+
+    @Schema(description = "鏍瑰洜鍒嗙被", example = "3")
+    private Integer rootCauseCategory;
+
+    @Schema(description = "鐘舵��: 0-鑽夌 1-宸插彂甯� 2-宸插仠鐢�", example = "1")
+    private Integer status;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java
new file mode 100644
index 0000000..2d10593
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
+
+import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 鍞悗闂搴� - 鍝嶅簲 VO
+ */
+@Schema(description = "鍞悗闂搴� - 鍝嶅簲 VO")
+@Data
+public class AfterSaleProblemRespVO {
+
+    @Schema(description = "闂缂栧彿", example = "1")
+    private Long id;
+
+    @Schema(description = "闂鍗曞彿", example = "AP20260813000001")
+    private String no;
+
+    @Schema(description = "闂鏍囬", example = "鎷夊姏鏈轰紶鎰熷櫒鏃犺緭鍑�")
+    private String title;
+
+    @Schema(description = "鏉ユ簮宸ュ崟缂栧彿", example = "1")
+    private Long sourceTicketId;
+
+    @Schema(description = "鏉ユ簮宸ュ崟鍗曞彿(鍐椾綑)", example = "AS20260813000001")
+    private String sourceTicketNo;
+
+    @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+    private Long customerId;
+
+    @Schema(description = "瀹㈡埛鍚嶇О")
+    private String customerName;
+
+    @Schema(description = "鐗╂枡缂栧彿", example = "1")
+    private Long itemId;
+
+    @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+    private String itemCode;
+
+    @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+    private String itemName;
+
+    @Schema(description = "鐢熶骇鎵规缂栧彿", example = "1")
+    private Long batchId;
+
+    @Schema(description = "鐢熶骇鎵规鍙�", example = "BATCH-2024-001")
+    private String batchCode;
+
+    @Schema(description = "闂绫诲瀷", example = "1")
+    private Integer issueType;
+
+    @Schema(description = "闂鍒嗙被", example = "鍔熻兘鏁呴殰")
+    private String issueCategory;
+
+    @Schema(description = "涓ラ噸绋嬪害", example = "2")
+    private Integer severityLevel;
+
+    @Schema(description = "闂鎻忚堪")
+    private String description;
+
+    @Schema(description = "鏍瑰洜鍒嗙被", example = "3")
+    private Integer rootCauseCategory;
+
+    @Schema(description = "鏍瑰洜鍒嗘瀽")
+    private String rootCause;
+
+    @Schema(description = "瑙e喅鏂规")
+    private String solution;
+
+    @Schema(description = "鐘舵��: 0-鑽夌 1-宸插彂甯� 2-宸插仠鐢�", example = "1")
+    private Integer status;
+
+    @Schema(description = "娌夋穩浜虹敤鎴风紪鍙�", example = "1")
+    private Long ownerUserId;
+
+    @Schema(description = "娌夋穩浜哄悕绉�")
+    private String ownerUserName;
+
+    @Schema(description = "鍙戝竷鏃堕棿")
+    private LocalDateTime publishTime;
+
+    @Schema(description = "澶囨敞")
+    private String remark;
+
+    @Schema(description = "鍒涘缓鑰�")
+    private String creator;
+
+    @Schema(description = "鍒涘缓鏃堕棿")
+    private LocalDateTime createTime;
+
+    @Schema(description = "鏇存柊鑰�")
+    private String updater;
+
+    @Schema(description = "鏇存柊鏃堕棿")
+    private LocalDateTime updateTime;
+
+    @Schema(description = "闄勪欢鍒楄〃")
+    private List<StorageBlobRespDTO> attachmentList;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java
new file mode 100644
index 0000000..46ea45b
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java
@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 鍞悗闂搴� - 淇濆瓨 Request VO
+ */
+@Schema(description = "鍞悗闂搴� - 淇濆瓨 Request VO")
+@Data
+public class AfterSaleProblemSaveReqVO {
+
+    @Schema(description = "闂缂栧彿", example = "1")
+    private Long id;
+
+    @Schema(description = "闂鏍囬", requiredMode = Schema.RequiredMode.REQUIRED, example = "鎷夊姏鏈轰紶鎰熷櫒鏃犺緭鍑�")
+    @NotEmpty(message = "闂鏍囬涓嶈兘涓虹┖")
+    private String title;
+
+    @Schema(description = "鏉ユ簮宸ュ崟缂栧彿(鍙�夛紝渚夸簬杩芥函)", example = "1")
+    private Long sourceTicketId;
+
+    @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+    private Long customerId;
+
+    @Schema(description = "鐗╂枡缂栧彿", example = "1")
+    private Long itemId;
+
+    @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+    private String itemCode;
+
+    @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+    private String itemName;
+
+    @Schema(description = "鐢熶骇鎵规缂栧彿", example = "1")
+    private Long batchId;
+
+    @Schema(description = "鐢熶骇鎵规鍙�", example = "BATCH-2024-001")
+    private String batchCode;
+
+    @Schema(description = "闂绫诲瀷: 0-鏈垽瀹� 1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰�", example = "1")
+    private Integer issueType;
+
+    @Schema(description = "闂鍒嗙被", example = "鍔熻兘鏁呴殰")
+    private String issueCategory;
+
+    @Schema(description = "涓ラ噸绋嬪害: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡", example = "2")
+    private Integer severityLevel;
+
+    @Schema(description = "闂鎻忚堪")
+    private String description;
+
+    @Schema(description = "鏍瑰洜鍒嗙被: 1-璁捐 2-鐗╂枡 3-宸ヨ壓 4-瑁呴厤 5-杩愯緭 6-鍖呰 7-浣跨敤 8-杞欢 9-鍏朵粬", example = "3")
+    private Integer rootCauseCategory;
+
+    @Schema(description = "鏍瑰洜鍒嗘瀽")
+    private String rootCause;
+
+    @Schema(description = "瑙e喅鏂规")
+    private String solution;
+
+    @Schema(description = "澶囨敞")
+    private String remark;
+
+    @Schema(description = "闄勪欢 blobId 鍒楄〃")
+    private List<Long> blobIds;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
index eef8955..b8295ce 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
@@ -33,6 +33,9 @@
     @Schema(description = "闂绫诲瀷", example = "1")
     private Integer issueType;
 
+    @Schema(description = "鍥炶鐘舵��: 0-寰呭洖璁� 1-宸插洖璁块�氳繃", example = "0")
+    private Integer visitStatus;
+
     @Schema(description = "鍦烘櫙绫诲瀷", example = "1")
     private Integer sceneType;
 
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
index f8031a1..dc16302 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
@@ -87,6 +87,24 @@
     @Schema(description = "鍏抽棴鏃堕棿")
     private LocalDateTime closedTime;
 
+    @Schema(description = "鍥炶鐘舵��: 0-寰呭洖璁� 1-宸插洖璁块�氳繃", example = "0")
+    private Integer visitStatus;
+
+    @Schema(description = "鏈�杩戜竴娆″洖璁挎椂闂�")
+    private LocalDateTime lastVisitTime;
+
+    @Schema(description = "鏈�杩戜竴娆″洖璁挎槸鍚︽弧鎰�", example = "true")
+    private Boolean lastVisitSatisfied;
+
+    @Schema(description = "鏈�杩戜竴娆″洖璁块棶棰樻槸鍚﹀鍙�", example = "false")
+    private Boolean lastVisitRecurred;
+
+    @Schema(description = "娌夋穩闂缂栧彿", example = "1")
+    private Long problemId;
+
+    @Schema(description = "娌夋穩闂鍗曞彿", example = "AP20260813000001")
+    private String problemNo;
+
     @Schema(description = "澶囨敞")
     private String remark;
 
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
index 6e7c96f..0152e20 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
@@ -38,6 +38,9 @@
     @Schema(description = "鍚堝悓缂栧彿", example = "1")
     private Long contractId;
 
+    @Schema(description = "澶勭悊浜虹敤鎴风紪鍙�(鍙�夛紝浠呰崏绋挎�佹湁鏁堬紱澶勭悊浜哄垎閰嶅缓璁蛋 assign 鎺ュ彛)", example = "2")
+    private Long handlerUserId;
+
     @Schema(description = "闂绫诲瀷: 1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰�", example = "1")
     private Integer issueType;
 
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java
new file mode 100644
index 0000000..e168ea8
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java
@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.visit;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitRespVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
+import cn.iocoder.yudao.module.aftersales.service.visit.AfterSaleVisitService;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - 鍞悗鍥炶璁板綍")
+@RestController
+@RequestMapping("/aftersales/visit")
+@Validated
+public class AfterSaleVisitController {
+
+    @Resource
+    private AfterSaleVisitService visitService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private StorageAttachmentApi storageAttachmentApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "鐧昏鍞悗鍥炶")
+    @PreAuthorize("@ss.hasPermission('aftersales:visit:create')")
+    public CommonResult<Long> createVisit(@Valid @RequestBody AfterSaleVisitCreateReqVO createReqVO) {
+        return success(visitService.createVisit(createReqVO, getLoginUserId()));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "鑾峰緱鍞悗鍥炶璁板綍")
+    @Parameter(name = "id", description = "鍥炶璁板綍缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
+    public CommonResult<AfterSaleVisitRespVO> getVisit(@RequestParam("id") Long id) {
+        AfterSaleVisitDO visitDO = visitService.getVisit(id);
+        AfterSaleVisitRespVO respVO = buildVisitDetailList(List.of(visitDO)).get(0);
+        respVO.setAttachmentList(storageAttachmentApi.listAttachments("aftersales_after_sale_visit", id));
+        return success(respVO);
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "鑾峰緱鏌愬伐鍗曠殑鍥炶璁板綍鍒楄〃")
+    @Parameter(name = "ticketId", description = "宸ュ崟缂栧彿", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
+    public CommonResult<List<AfterSaleVisitRespVO>> getVisitListByTicketId(@RequestParam("ticketId") Long ticketId) {
+        List<AfterSaleVisitDO> visitList = visitService.getVisitListByTicketId(ticketId);
+        return success(buildVisitDetailList(visitList));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "鑾峰緱鍞悗鍥炶璁板綍鍒嗛〉")
+    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
+    public CommonResult<PageResult<AfterSaleVisitRespVO>> getVisitPage(@Valid AfterSaleVisitPageReqVO pageReqVO) {
+        PageResult<AfterSaleVisitDO> pageResult = visitService.getVisitPage(pageReqVO);
+        return success(new PageResult<>(buildVisitDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    // ========== 鍐呴儴鏂规硶锛氭壒閲忔嫾瑁呭叧鑱斿悕绉� ==========
+
+    private List<AfterSaleVisitRespVO> buildVisitDetailList(List<AfterSaleVisitDO> visitList) {
+        if (CollUtil.isEmpty(visitList)) {
+            return Collections.emptyList();
+        }
+        // 鎵归噺鍔犺浇鍥炶浜哄悕绉�
+        Set<Long> userIds = convertSet(visitList, AfterSaleVisitDO::getVisitorUserId);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        return BeanUtils.toBean(visitList, AfterSaleVisitRespVO.class, vo -> {
+            AdminUserRespDTO visitorUser = userMap.get(vo.getVisitorUserId());
+            if (visitorUser != null) {
+                vo.setVisitorUserName(visitorUser.getNickname());
+            }
+        });
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java
new file mode 100644
index 0000000..423fc34
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java
@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 鍞悗鍥炶璁板綍 - 鐧昏 Request VO
+ */
+@Schema(description = "鍞悗鍥炶璁板綍 - 鐧昏 Request VO")
+@Data
+public class AfterSaleVisitCreateReqVO {
+
+    @Schema(description = "宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "宸ュ崟涓嶈兘涓虹┖")
+    private Long ticketId;
+
+    @Schema(description = "鍥炶鏂瑰紡: 0-鐢佃瘽 1-涓婇棬 2-杩滅▼ 3-鍏朵粬", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @NotNull(message = "鍥炶鏂瑰紡涓嶈兘涓虹┖")
+    private Integer visitType;
+
+    @Schema(description = "鍥炶鍐呭", example = "瀹㈡埛鍙嶉宸叉甯镐娇鐢�")
+    private String content;
+
+    @Schema(description = "鏄惁婊℃剰", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "鏄惁婊℃剰涓嶈兘涓虹┖")
+    private Boolean satisfied;
+
+    @Schema(description = "闂鏄惁澶嶅彂", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "闂鏄惁澶嶅彂涓嶈兘涓虹┖")
+    private Boolean recurred;
+
+    @Schema(description = "澶囨敞")
+    private String remark;
+
+    @Schema(description = "闄勪欢 blobId 鍒楄〃")
+    private List<Long> blobIds;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java
new file mode 100644
index 0000000..627cf22
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 鍞悗鍥炶璁板綍 - 鍒嗛〉鏌ヨ Request VO
+ */
+@Schema(description = "鍞悗鍥炶璁板綍 - 鍒嗛〉鏌ヨ Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AfterSaleVisitPageReqVO extends PageParam {
+
+    @Schema(description = "宸ュ崟缂栧彿", example = "1")
+    private Long ticketId;
+
+    @Schema(description = "鍥炶鏂瑰紡", example = "0")
+    private Integer visitType;
+
+    @Schema(description = "鏄惁婊℃剰", example = "true")
+    private Boolean satisfied;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java
new file mode 100644
index 0000000..9299605
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
+
+import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 鍞悗鍥炶璁板綍 - 鍝嶅簲 VO
+ */
+@Schema(description = "鍞悗鍥炶璁板綍 - 鍝嶅簲 VO")
+@Data
+public class AfterSaleVisitRespVO {
+
+    @Schema(description = "鍥炶璁板綍缂栧彿", example = "1")
+    private Long id;
+
+    @Schema(description = "宸ュ崟缂栧彿", example = "1")
+    private Long ticketId;
+
+    @Schema(description = "鍥炶鏂瑰紡: 0-鐢佃瘽 1-涓婇棬 2-杩滅▼ 3-鍏朵粬", example = "0")
+    private Integer visitType;
+
+    @Schema(description = "鍥炶浜虹敤鎴风紪鍙�", example = "1")
+    private Long visitorUserId;
+
+    @Schema(description = "鍥炶浜哄悕绉�")
+    private String visitorUserName;
+
+    @Schema(description = "鍥炶鍐呭")
+    private String content;
+
+    @Schema(description = "鏄惁婊℃剰", example = "true")
+    private Boolean satisfied;
+
+    @Schema(description = "闂鏄惁澶嶅彂", example = "false")
+    private Boolean recurred;
+
+    @Schema(description = "鍥炶鏃堕棿")
+    private LocalDateTime visitTime;
+
+    @Schema(description = "澶囨敞")
+    private String remark;
+
+    @Schema(description = "鍒涘缓鑰�")
+    private String creator;
+
+    @Schema(description = "鍒涘缓鏃堕棿")
+    private LocalDateTime createTime;
+
+    @Schema(description = "闄勪欢鍒楄〃")
+    private List<StorageBlobRespDTO> attachmentList;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java
new file mode 100644
index 0000000..dfd6b07
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java
@@ -0,0 +1,118 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.problem;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleProblemStatusEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗闂搴� DO
+ * <p>
+ * 鐙珛鐨勭煡璇嗘矇娣�瀹炰綋锛氬伐鍗曞綊妗f椂鎸夊伐鍗曞揩鐓ц嚜鍔ㄧ敓鎴愯崏绋匡紝浜哄伐琛ュ厖鏍瑰洜/鏂规鍚庡彂甯冿紱
+ * 鏂板伐鍗曞彈鐞嗘椂鍙寜鐗╂枡/鍒嗙被/鍏抽敭瀛楀懡涓凡鍙戝竷闂浣滃弬鑰冦��
+ */
+@TableName("after_sale_problem")
+@KeySequence("after_sale_problem_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleProblemDO extends BaseDO {
+
+    /**
+     * 闂缂栧彿
+     */
+    @TableId
+    private Long id;
+    /**
+     * 闂鍗曞彿锛屾牸寮� AP+yyyyMMdd+6浣�
+     */
+    private String no;
+    /**
+     * 闂鏍囬
+     */
+    private String title;
+    /**
+     * 鏉ユ簮宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+     */
+    private Long sourceTicketId;
+    /**
+     * 鏉ユ簮宸ュ崟鍗曞彿(鍐椾綑)
+     */
+    private String sourceTicketNo;
+    /**
+     * 瀹㈡埛缂栧彿(鏉ユ簮宸ュ崟甯﹀叆)
+     */
+    private Long customerId;
+    /**
+     * 鐗╂枡缂栧彿(榛樿鍙栨潵婧愬伐鍗曢鏉℃槑缁�)
+     */
+    private Long itemId;
+    /**
+     * 鐗╂枡缂栫爜(鍐椾綑)
+     */
+    private String itemCode;
+    /**
+     * 鐗╂枡鍚嶇О(鍐椾綑)
+     */
+    private String itemName;
+    /**
+     * 鐢熶骇鎵规缂栧彿(鍙��)
+     */
+    private Long batchId;
+    /**
+     * 鐢熶骇鎵规鍙�(鍙��)
+     */
+    private String batchCode;
+    /**
+     * 闂绫诲瀷锛屾灇涓� {@link cn.iocoder.yudao.module.aftersales.enums.AfterSaleIssueTypeEnum}
+     */
+    private Integer issueType;
+    /**
+     * 闂鍒嗙被
+     */
+    private String issueCategory;
+    /**
+     * 涓ラ噸绋嬪害: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡
+     */
+    private Integer severityLevel;
+    /**
+     * 闂鎻忚堪
+     */
+    private String description;
+    /**
+     * 鏍瑰洜鍒嗙被锛屾灇涓� {@link cn.iocoder.yudao.module.aftersales.enums.AfterSaleRootCauseCategoryEnum}
+     */
+    private Integer rootCauseCategory;
+    /**
+     * 鏍瑰洜鍒嗘瀽
+     */
+    private String rootCause;
+    /**
+     * 瑙e喅鏂规
+     */
+    private String solution;
+    /**
+     * 鐘舵�侊紝鏋氫妇 {@link AfterSaleProblemStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 娌夋穩浜虹敤鎴风紪鍙�
+     */
+    private Long ownerUserId;
+    /**
+     * 鍙戝竷鏃堕棿
+     */
+    private LocalDateTime publishTime;
+    /**
+     * 澶囨敞
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
index a2eee67..4f71c3e 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
@@ -109,6 +109,32 @@
      */
     private LocalDateTime closedTime;
     /**
+     * 鍥炶鐘舵��: 0-寰呭洖璁� 1-宸插洖璁块�氳繃
+     *
+     * 瀹岀粨(COMPLETED)鍚庤繘鍏ュ緟鍥炶锛岀櫥璁般�屾弧鎰忎笖鏈鍙戙�嶇殑鍥炶鍚庣疆涓哄凡鍥炶閫氳繃锛屾柟鍙綊妗�
+     */
+    private Integer visitStatus;
+    /**
+     * 鏈�杩戜竴娆″洖璁挎椂闂�
+     */
+    private LocalDateTime lastVisitTime;
+    /**
+     * 鏈�杩戜竴娆″洖璁挎槸鍚︽弧鎰�
+     */
+    private Boolean lastVisitSatisfied;
+    /**
+     * 鏈�杩戜竴娆″洖璁块棶棰樻槸鍚﹀鍙�
+     */
+    private Boolean lastVisitRecurred;
+    /**
+     * 娌夋穩闂缂栧彿锛屽叧鑱� after_sale_problem.id
+     */
+    private Long problemId;
+    /**
+     * 娌夋穩闂鍗曞彿(鍐椾綑)
+     */
+    private String problemNo;
+    /**
      * 澶囨敞
      */
     private String remark;
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java
new file mode 100644
index 0000000..87b5363
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.visit;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗鍥炶璁板綍 DO
+ * <p>
+ * 鎸傚湪鍞悗宸ュ崟涓嬬殑鍥炶瀛愯褰曪紝鍙鏉★紱鐧昏鍥炶鏃跺悓姝ユ帹杩涘伐鍗曞洖璁跨姸鎬併��
+ */
+@TableName("after_sale_visit")
+@KeySequence("after_sale_visit_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleVisitDO extends BaseDO {
+
+    /**
+     * 鍥炶璁板綍缂栧彿
+     */
+    @TableId
+    private Long id;
+    /**
+     * 宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+     */
+    private Long ticketId;
+    /**
+     * 鍥炶鏂瑰紡: 0-鐢佃瘽 1-涓婇棬 2-杩滅▼ 3-鍏朵粬
+     */
+    private Integer visitType;
+    /**
+     * 鍥炶浜虹敤鎴风紪鍙�
+     */
+    private Long visitorUserId;
+    /**
+     * 鍥炶鍐呭
+     */
+    private String content;
+    /**
+     * 鏄惁婊℃剰: 0-鍚� 1-鏄�
+     */
+    @TableField("is_satisfied")
+    private Boolean satisfied;
+    /**
+     * 闂鏄惁澶嶅彂: 0-鍚� 1-鏄�
+     */
+    @TableField("is_recurred")
+    private Boolean recurred;
+    /**
+     * 鍥炶鏃堕棿(鐧昏鍗冲綋鏃�)
+     */
+    private LocalDateTime visitTime;
+    /**
+     * 澶囨敞
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java
new file mode 100644
index 0000000..db9efec
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.problem;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleProblemStatusEnum;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗闂搴� Mapper
+ */
+@Mapper
+public interface AfterSaleProblemMapper extends BaseMapperX<AfterSaleProblemDO> {
+
+    default AfterSaleProblemDO selectByNo(String no) {
+        return selectOne(AfterSaleProblemDO::getNo, no);
+    }
+
+    default AfterSaleProblemDO selectBySourceTicketId(Long sourceTicketId) {
+        return selectOne(AfterSaleProblemDO::getSourceTicketId, sourceTicketId);
+    }
+
+    default PageResult<AfterSaleProblemDO> selectPage(PageParam pageParam, String no, String keyword,
+                                                      Long sourceTicketId, Long itemId, String itemCode,
+                                                      String issueCategory,
+                                                      Integer issueType, Integer rootCauseCategory, Integer status) {
+        LambdaQueryWrapperX<AfterSaleProblemDO> query = new LambdaQueryWrapperX<AfterSaleProblemDO>()
+                .eqIfPresent(AfterSaleProblemDO::getNo, no)
+                .eqIfPresent(AfterSaleProblemDO::getSourceTicketId, sourceTicketId)
+                .eqIfPresent(AfterSaleProblemDO::getItemId, itemId)
+                .likeIfPresent(AfterSaleProblemDO::getItemCode, itemCode)
+                .eqIfPresent(AfterSaleProblemDO::getIssueCategory, issueCategory)
+                .eqIfPresent(AfterSaleProblemDO::getIssueType, issueType)
+                .eqIfPresent(AfterSaleProblemDO::getRootCauseCategory, rootCauseCategory)
+                .eqIfPresent(AfterSaleProblemDO::getStatus, status);
+        // 鍏抽敭瀛楁绱細鍗曞彿/鏍囬/鎻忚堪/瑙e喅鏂规
+        if (StrUtil.isNotBlank(keyword)) {
+            query.and(w -> w.like(AfterSaleProblemDO::getNo, keyword)
+                    .or().like(AfterSaleProblemDO::getTitle, keyword)
+                    .or().like(AfterSaleProblemDO::getDescription, keyword)
+                    .or().like(AfterSaleProblemDO::getSolution, keyword));
+        }
+        query.orderByDesc(AfterSaleProblemDO::getUpdateTime);
+        return selectPage(pageParam, query);
+    }
+
+    /**
+     * 鏂板伐鍗曞彈鐞嗗弬鑰冩绱細鍙繑鍥炲凡鍙戝竷锛屾寜鐗╂枡/鍒嗙被/鍏抽敭瀛楀懡涓紝鍙栨渶杩戞洿鏂� 10 鏉�
+     */
+    default List<AfterSaleProblemDO> selectReferenceList(Long itemId, String issueCategory, Integer issueType,
+                                                         Integer rootCauseCategory, String keyword) {
+        LambdaQueryWrapperX<AfterSaleProblemDO> query = new LambdaQueryWrapperX<AfterSaleProblemDO>()
+                .eq(AfterSaleProblemDO::getStatus, AfterSaleProblemStatusEnum.PUBLISHED.getStatus());
+        query.and(w -> {
+                    boolean hasCond = false;
+                    if (itemId != null) {
+                        w.eq(AfterSaleProblemDO::getItemId, itemId);
+                        hasCond = true;
+                    }
+                    if (StrUtil.isNotBlank(issueCategory)) {
+                        if (hasCond) {
+                            w.or();
+                        }
+                        w.eq(AfterSaleProblemDO::getIssueCategory, issueCategory);
+                        hasCond = true;
+                    }
+                    if (issueType != null) {
+                        if (hasCond) {
+                            w.or();
+                        }
+                        w.eq(AfterSaleProblemDO::getIssueType, issueType);
+                        hasCond = true;
+                    }
+                    if (rootCauseCategory != null) {
+                        if (hasCond) {
+                            w.or();
+                        }
+                        w.eq(AfterSaleProblemDO::getRootCauseCategory, rootCauseCategory);
+                        hasCond = true;
+                    }
+                    if (StrUtil.isNotBlank(keyword)) {
+                        if (hasCond) {
+                            w.or();
+                        }
+                        w.like(AfterSaleProblemDO::getTitle, keyword);
+                        hasCond = true;
+                    }
+                    if (!hasCond) {
+                        w.eq(AfterSaleProblemDO::getId, -1L); // 鏃犳潯浠舵椂杩斿洖绌猴紝閬垮厤鍏ㄨ〃鎷夊彇
+                    }
+                });
+        query.orderByDesc(AfterSaleProblemDO::getUpdateTime).last("LIMIT 10");
+        return selectList(query);
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
index 2546020..319d61f 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
@@ -25,7 +25,7 @@
     default PageResult<AfterSaleTicketDO> selectPage(PageParam pageParam, Long userId, Integer sceneType,
                                                      String no, String name,
                                                      Long customerId, Long saleOrderId,
-                                                     Integer status, Integer issueType) {
+                                                     Integer status, Integer issueType, Integer visitStatus) {
         MPJLambdaWrapperX<AfterSaleTicketDO> query = new MPJLambdaWrapperX<>();
         // 鏁版嵁鏉冮檺
         AfterSalePermissionUtils.appendPermissionCondition(query, AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(),
@@ -38,6 +38,7 @@
                 .eqIfPresent(AfterSaleTicketDO::getSaleOrderId, saleOrderId)
                 .eqIfPresent(AfterSaleTicketDO::getStatus, status)
                 .eqIfPresent(AfterSaleTicketDO::getIssueType, issueType)
+                .eqIfPresent(AfterSaleTicketDO::getVisitStatus, visitStatus)
                 .orderByDesc(AfterSaleTicketDO::getId);
         return selectJoinPage(pageParam, AfterSaleTicketDO.class, query);
     }
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java
new file mode 100644
index 0000000..060adcb
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.visit;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗鍥炶璁板綍 Mapper
+ */
+@Mapper
+public interface AfterSaleVisitMapper extends BaseMapperX<AfterSaleVisitDO> {
+
+    default List<AfterSaleVisitDO> selectListByTicketId(Long ticketId) {
+        return selectList(new LambdaQueryWrapperX<AfterSaleVisitDO>()
+                .eq(AfterSaleVisitDO::getTicketId, ticketId)
+                .orderByDesc(AfterSaleVisitDO::getVisitTime));
+    }
+
+    default PageResult<AfterSaleVisitDO> selectPage(PageParam pageParam, Long ticketId, Integer visitType,
+                                                    Boolean satisfied) {
+        return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleVisitDO>()
+                .eqIfPresent(AfterSaleVisitDO::getTicketId, ticketId)
+                .eqIfPresent(AfterSaleVisitDO::getVisitType, visitType)
+                .eqIfPresent(AfterSaleVisitDO::getSatisfied, satisfied)
+                .orderByDesc(AfterSaleVisitDO::getVisitTime));
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
index 856c862..ab2b488 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -26,6 +26,11 @@
      */
     public static final String RETURN_NO_PREFIX = "RT";
 
+    /**
+     * 鍞悗闂搴� {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO}
+     */
+    public static final String PROBLEM_NO_PREFIX = "AP";
+
     @Resource
     private StringRedisTemplate stringRedisTemplate;
 
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java
new file mode 100644
index 0000000..c90eb7e
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗闂搴撶姸鎬佹灇涓�
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSaleProblemStatusEnum implements ArrayValuable<Integer> {
+
+    DRAFT(0, "鑽夌"),
+    PUBLISHED(1, "宸插彂甯�"),
+    DISABLED(2, "宸插仠鐢�");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleProblemStatusEnum::getStatus).toArray(Integer[]::new);
+
+    /**
+     * 闂鐘舵��
+     */
+    private final Integer status;
+    /**
+     * 闂鐘舵�佸悕绉�
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static String getNameByStatus(Integer status) {
+        return Arrays.stream(values())
+                .filter(item -> item.getStatus().equals(status))
+                .map(AfterSaleProblemStatusEnum::getName)
+                .findFirst().orElse(null);
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRootCauseCategoryEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRootCauseCategoryEnum.java
new file mode 100644
index 0000000..fa7ce01
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRootCauseCategoryEnum.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗闂鏍瑰洜鍒嗙被鏋氫妇
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSaleRootCauseCategoryEnum implements ArrayValuable<Integer> {
+
+    DESIGN(1, "璁捐缂洪櫡"),
+    MATERIAL(2, "鐗╂枡缂洪櫡"),
+    PROCESS(3, "宸ヨ壓缂洪櫡"),
+    ASSEMBLY(4, "瑁呴厤缂洪櫡"),
+    TRANSPORT(5, "杩愯緭鎹熷潖"),
+    PACKAGING(6, "鍖呰缂洪櫡"),
+    USAGE(7, "浣跨敤涓嶅綋"),
+    SOFTWARE(8, "杞欢缂洪櫡"),
+    OTHER(9, "鍏朵粬");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleRootCauseCategoryEnum::getType).toArray(Integer[]::new);
+
+    /**
+     * 鏍瑰洜鍒嗙被
+     */
+    private final Integer type;
+    /**
+     * 鏍瑰洜鍒嗙被鍚嶇О
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static String getNameByType(Integer type) {
+        return Arrays.stream(values())
+                .filter(item -> item.getType().equals(type))
+                .map(AfterSaleRootCauseCategoryEnum::getName)
+                .findFirst().orElse(null);
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
index 88f6da8..088ad02 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
@@ -17,7 +17,9 @@
     PROCESSING(10, "澶勭悊涓�"),
     WAITING_RETURN(30, "寰呴��璐�"),
     WAITING_REFUND(40, "寰呴��娆�/鎹㈣揣"),
+    // 50 宸插畬缁擄細璇箟=寰呭洖璁匡紝鐧昏銆屾弧鎰忎笖鏈鍙戙�嶇殑鍥炶閫氳繃鍚庢柟鍙綊妗�
     COMPLETED(50, "宸插畬缁�"),
+    // 60 宸插叧闂細瑙嗕负褰掓。缁堟��(鍙)锛屽綊妗f椂鑷姩娌夋穩涓�鏉¢棶棰樺簱鑽夌
     CLOSED(60, "宸插叧闂�"),
     CANCELED(99, "宸插彇娑�");
 
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java
new file mode 100644
index 0000000..8c08872
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗宸ュ崟鍥炶鐘舵�佹灇涓�
+ * <p>
+ * 宸ュ崟瀹岀粨(COMPLETED)鍚庤繘鍏ャ�屽緟鍥炶銆嶏紱鐧昏銆屾弧鎰忎笖鏈鍙戙�嶇殑鍥炶鍚庣疆涓恒�屽凡鍥炶閫氳繃銆嶏紝鏂瑰彲褰掓。銆�
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSaleVisitStatusEnum implements ArrayValuable<Integer> {
+
+    WAITING(0, "寰呭洖璁�"),
+    PASSED(1, "宸插洖璁块�氳繃");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleVisitStatusEnum::getStatus).toArray(Integer[]::new);
+
+    /**
+     * 鍥炶鐘舵��
+     */
+    private final Integer status;
+    /**
+     * 鍥炶鐘舵�佸悕绉�
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static String getNameByStatus(Integer status) {
+        return Arrays.stream(values())
+                .filter(item -> item.getStatus().equals(status))
+                .map(AfterSaleVisitStatusEnum::getName)
+                .findFirst().orElse(null);
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitTypeEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitTypeEnum.java
new file mode 100644
index 0000000..348d394
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitTypeEnum.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗鍥炶鏂瑰紡鏋氫妇
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSaleVisitTypeEnum implements ArrayValuable<Integer> {
+
+    PHONE(0, "鐢佃瘽鍥炶"),
+    ON_SITE(1, "涓婇棬鍥炶"),
+    REMOTE(2, "杩滅▼鍥炶"),
+    OTHER(3, "鍏朵粬");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleVisitTypeEnum::getType).toArray(Integer[]::new);
+
+    /**
+     * 鍥炶鏂瑰紡
+     */
+    private final Integer type;
+    /**
+     * 鍥炶鏂瑰紡鍚嶇О
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static String getNameByType(Integer type) {
+        return Arrays.stream(values())
+                .filter(item -> item.getType().equals(type))
+                .map(AfterSaleVisitTypeEnum::getName)
+                .findFirst().orElse(null);
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
index 1796711..d49adcf 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
@@ -19,6 +19,7 @@
     ErrorCode TICKET_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_022_000_006, "鎻愪氦宸ュ崟瀹℃壒澶辫触锛屽師鍥狅細宸ュ崟涓嶆槸鑽夌鐘舵��");
     ErrorCode TICKET_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_022_000_007, "鍞悗宸ュ崟瀹℃壒娴佺▼涓嶅瓨鍦ㄦ垨鏈縺娲伙紝璇锋鏌� BPM 閰嶇疆");
     ErrorCode TICKET_STATUS_ERROR = new ErrorCode(1_022_000_008, "宸ュ崟褰撳墠鐘舵�佷笉鍏佽姝ゆ搷浣�");
+    ErrorCode TICKET_ARCHIVE_FAIL_NOT_VISITED = new ErrorCode(1_022_000_009, "褰掓。澶辫触锛屽師鍥狅細宸ュ崟灏氭湭鍥炶閫氳繃锛岃鍏堢櫥璁版弧鎰忕殑鍥炶璁板綍");
 
     // ========== 鍞悗宸ュ崟鏄庣粏琛� 1-022-001-000 ==========
     ErrorCode TICKET_ITEM_NOT_EXISTS = new ErrorCode(1_022_001_000, "宸ュ崟鏄庣粏琛屼笉瀛樺湪");
@@ -35,4 +36,16 @@
     ErrorCode REPAIR_NOT_EXISTS = new ErrorCode(1_022_003_000, "缁翠慨璁板綍涓嶅瓨鍦�");
     ErrorCode REPAIR_UPDATE_FAIL_STATUS = new ErrorCode(1_022_003_001, "鏇存柊缁翠慨鐘舵�佸け璐ワ紝鍘熷洜锛氬凡缁忔槸璇ョ姸鎬�");
 
+    // ========== 鍞悗鍥炶 1-022-004-000 ==========
+    ErrorCode VISIT_NOT_EXISTS = new ErrorCode(1_022_004_000, "鍥炶璁板綍涓嶅瓨鍦�");
+    ErrorCode VISIT_CREATE_FAIL_STATUS = new ErrorCode(1_022_004_001, "鐧昏鍥炶澶辫触锛屽師鍥狅細宸ュ崟涓嶆槸宸插畬缁�(寰呭洖璁�)鐘舵��");
+
+    // ========== 鍞悗闂搴� 1-022-005-000 ==========
+    ErrorCode PROBLEM_NOT_EXISTS = new ErrorCode(1_022_005_000, "闂璁板綍涓嶅瓨鍦�");
+    ErrorCode PROBLEM_NO_EXISTS = new ErrorCode(1_022_005_001, "闂鍗曞彿閲嶅锛岃閲嶈瘯");
+    ErrorCode PROBLEM_UPDATE_FAIL_STATUS = new ErrorCode(1_022_005_002, "闂淇敼澶辫触锛屽師鍥狅細浠呰崏绋跨姸鎬佸彲缂栬緫");
+    ErrorCode PROBLEM_DELETE_FAIL_STATUS = new ErrorCode(1_022_005_003, "闂鍒犻櫎澶辫触锛屽師鍥狅細浠呰崏绋跨姸鎬佸彲鍒犻櫎");
+    ErrorCode PROBLEM_PUBLISH_FAIL_STATUS = new ErrorCode(1_022_005_004, "闂鍙戝竷澶辫触锛屽師鍥狅細褰撳墠鐘舵�佷笉鍙彂甯�");
+    ErrorCode PROBLEM_DISABLE_FAIL_STATUS = new ErrorCode(1_022_005_005, "闂鍋滅敤澶辫触锛屽師鍥狅細褰撳墠鐘舵�佷笉鍙仠鐢�");
+
 }
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
index 0fd4527..3d8047b 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
@@ -45,4 +45,24 @@
     String AFTER_SALE_REPAIR_COMPLETE_SUB_TYPE = "瀹屾垚缁翠慨";
     String AFTER_SALE_REPAIR_COMPLETE_SUCCESS = "瀹屾垚浜嗙淮淇褰曘�恵{#repairId}}銆戯紝缁翠慨缁撴灉锛氥�恵{#repairResult}}銆�";
 
+    // ======================= 鍞悗鍥炶 =======================
+
+    String AFTER_SALE_VISIT_TYPE = "鍞悗鍥炶";
+    String AFTER_SALE_VISIT_CREATE_SUB_TYPE = "鐧昏鍥炶";
+    String AFTER_SALE_VISIT_CREATE_SUCCESS = "鐧昏浜嗗敭鍚庡伐鍗曘�恵{#ticketNo}}銆戠殑鍥炶锛氭弧鎰忋�恵{#satisfied}}銆�/澶嶅彂銆恵{#recurred}}銆�";
+
+    // ======================= 鍞悗闂搴� =======================
+
+    String AFTER_SALE_PROBLEM_TYPE = "鍞悗闂搴�";
+    String AFTER_SALE_PROBLEM_CREATE_SUB_TYPE = "鍒涘缓闂";
+    String AFTER_SALE_PROBLEM_CREATE_SUCCESS = "鍒涘缓浜嗗敭鍚庨棶棰樸�恵{#problem.title}}銆�";
+    String AFTER_SALE_PROBLEM_UPDATE_SUB_TYPE = "鏇存柊闂";
+    String AFTER_SALE_PROBLEM_UPDATE_SUCCESS = "鏇存柊浜嗗敭鍚庨棶棰樸�恵{#problemTitle}}銆�: {_DIFF{#updateReqVO}}";
+    String AFTER_SALE_PROBLEM_DELETE_SUB_TYPE = "鍒犻櫎闂";
+    String AFTER_SALE_PROBLEM_DELETE_SUCCESS = "鍒犻櫎浜嗗敭鍚庨棶棰樸�恵{#problemTitle}}銆�";
+    String AFTER_SALE_PROBLEM_PUBLISH_SUB_TYPE = "鍙戝竷闂";
+    String AFTER_SALE_PROBLEM_PUBLISH_SUCCESS = "鍙戝竷浜嗗敭鍚庨棶棰樸�恵{#problemNo}}銆�";
+    String AFTER_SALE_PROBLEM_DISABLE_SUB_TYPE = "鍋滅敤闂";
+    String AFTER_SALE_PROBLEM_DISABLE_SUCCESS = "鍋滅敤浜嗗敭鍚庨棶棰樸�恵{#problemNo}}銆�";
+
 }
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java
new file mode 100644
index 0000000..3decdb2
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.aftersales.service.problem;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗闂搴� Service 鎺ュ彛
+ */
+public interface AfterSaleProblemService {
+
+    /**
+     * 鍒涘缓闂(浜哄伐寤烘。)锛屽垵濮嬬姸鎬佽崏绋�
+     *
+     * @param createReqVO 鍒涘缓淇℃伅
+     * @param userId      褰撳墠鐢ㄦ埛缂栧彿(娌夋穩浜�)
+     * @return 闂缂栧彿
+     */
+    Long createProblem(@Valid AfterSaleProblemSaveReqVO createReqVO, Long userId);
+
+    /**
+     * 鏇存柊闂
+     *
+     * @param updateReqVO 鏇存柊淇℃伅
+     */
+    void updateProblem(@Valid AfterSaleProblemSaveReqVO updateReqVO);
+
+    /**
+     * 鍒犻櫎闂(浠呰崏绋�)
+     *
+     * @param id 闂缂栧彿
+     */
+    void deleteProblem(Long id);
+
+    /**
+     * 鑾峰緱闂
+     *
+     * @param id 闂缂栧彿
+     * @return 闂
+     */
+    AfterSaleProblemDO getProblem(Long id);
+
+    /**
+     * 鑾峰緱闂鍒楄〃
+     *
+     * @param ids 缂栧彿鍒楄〃
+     * @return 闂鍒楄〃
+     */
+    List<AfterSaleProblemDO> getProblemList(Collection<Long> ids);
+
+    /**
+     * 鑾峰緱闂鍒嗛〉
+     *
+     * @param pageReqVO 鍒嗛〉鏌ヨ
+     * @return 闂鍒嗛〉
+     */
+    PageResult<AfterSaleProblemDO> getProblemPage(AfterSaleProblemPageReqVO pageReqVO);
+
+    /**
+     * 鍙戝竷闂
+     *
+     * @param id 闂缂栧彿
+     */
+    void publishProblem(Long id);
+
+    /**
+     * 鍋滅敤闂
+     *
+     * @param id 闂缂栧彿
+     */
+    void disableProblem(Long id);
+
+    /**
+     * 鏂板伐鍗曞彈鐞嗗弬鑰冩绱細鍛戒腑宸插彂甯冨巻鍙查棶棰�
+     *
+     * @param pageReqVO 鍙傝�冩绱㈡潯浠�(鍙� itemId/issueCategory/issueType/rootCauseCategory/keyword)
+     * @return 宸插彂甯冮棶棰樺垪琛�(鑷冲10鏉�)
+     */
+    List<AfterSaleProblemDO> getReferenceList(AfterSaleProblemPageReqVO pageReqVO);
+
+    /**
+     * 宸ュ崟褰掓。鏃惰嚜鍔ㄦ矇娣�闂(鑽夌鎬�)锛涘悓涓�宸ュ崟浠呮矇娣�涓�娆�(骞傜瓑)
+     *
+     * @param ticket 鍞悗宸ュ崟
+     * @return 娌夋穩鍑虹殑闂
+     */
+    AfterSaleProblemDO createProblemFromTicket(AfterSaleTicketDO ticket);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java
new file mode 100644
index 0000000..022162b
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java
@@ -0,0 +1,211 @@
+package cn.iocoder.yudao.module.aftersales.service.problem;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
+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.problem.AfterSaleProblemMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
+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.AfterSaleProblemStatusEnum;
+import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
+
+/**
+ * 鍞悗闂搴� Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+@Slf4j
+public class AfterSaleProblemServiceImpl implements AfterSaleProblemService {
+
+    @Resource
+    private AfterSaleProblemMapper problemMapper;
+    @Resource
+    private AfterSaleTicketMapper ticketMapper;
+    @Resource
+    private AfterSaleTicketItemMapper ticketItemMapper;
+    @Resource
+    private AfterSaleNoRedisDAO noRedisDAO;
+    @Resource
+    private StorageAttachmentApi storageAttachmentApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createProblem(AfterSaleProblemSaveReqVO createReqVO, Long userId) {
+        AfterSaleProblemDO problem = BeanUtils.toBean(createReqVO, AfterSaleProblemDO.class);
+        // 鏉ユ簮宸ュ崟瀛樺湪鏃惰ˉ鍏ㄥ啑浣欏崟鍙蜂笌榛樿瀹㈡埛
+        if (createReqVO.getSourceTicketId() != null) {
+            AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getSourceTicketId());
+            if (ticket == null) {
+                throw exception(TICKET_NOT_EXISTS);
+            }
+            problem.setSourceTicketNo(ticket.getNo());
+            if (problem.getCustomerId() == null) {
+                problem.setCustomerId(ticket.getCustomerId());
+            }
+        }
+        problem.setNo(generateNo())
+                .setStatus(AfterSaleProblemStatusEnum.DRAFT.getStatus())
+                .setOwnerUserId(userId);
+        problemMapper.insert(problem);
+        // 缁戝畾闄勪欢
+        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
+            storageAttachmentApi.bindAttachments("file", "aftersales_after_sale_problem",
+                    problem.getId(), createReqVO.getBlobIds());
+        }
+        return problem.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateProblem(AfterSaleProblemSaveReqVO updateReqVO) {
+        AfterSaleProblemDO problem = validateProblemExists(updateReqVO.getId());
+        if (!AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())) {
+            throw exception(PROBLEM_UPDATE_FAIL_STATUS);
+        }
+        AfterSaleProblemDO update = BeanUtils.toBean(updateReqVO, AfterSaleProblemDO.class);
+        problemMapper.updateById(update);
+        if (CollUtil.isNotEmpty(updateReqVO.getBlobIds())) {
+            storageAttachmentApi.updateAttachments("file", "aftersales_after_sale_problem",
+                    updateReqVO.getId(), updateReqVO.getBlobIds());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteProblem(Long id) {
+        AfterSaleProblemDO problem = validateProblemExists(id);
+        if (!AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())) {
+            throw exception(PROBLEM_DELETE_FAIL_STATUS);
+        }
+        problemMapper.deleteById(id);
+        storageAttachmentApi.deleteAttachmentsByRecord("aftersales_after_sale_problem", id);
+        // 瑙i櫎宸ュ崟瀵归棶棰樼殑鍏宠仈
+        ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
+                .eq(AfterSaleTicketDO::getProblemId, id)
+                .set(AfterSaleTicketDO::getProblemId, null)
+                .set(AfterSaleTicketDO::getProblemNo, null));
+    }
+
+    @Override
+    public AfterSaleProblemDO getProblem(Long id) {
+        return validateProblemExists(id);
+    }
+
+    @Override
+    public List<AfterSaleProblemDO> getProblemList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return List.of();
+        }
+        return problemMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<AfterSaleProblemDO> getProblemPage(AfterSaleProblemPageReqVO pageReqVO) {
+        return problemMapper.selectPage(pageReqVO, pageReqVO.getNo(), pageReqVO.getKeyword(),
+                pageReqVO.getSourceTicketId(), pageReqVO.getItemId(), pageReqVO.getItemCode(),
+                pageReqVO.getIssueCategory(),
+                pageReqVO.getIssueType(), pageReqVO.getRootCauseCategory(), pageReqVO.getStatus());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void publishProblem(Long id) {
+        AfterSaleProblemDO problem = validateProblemExists(id);
+        if (!(AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())
+                || AfterSaleProblemStatusEnum.DISABLED.getStatus().equals(problem.getStatus()))) {
+            throw exception(PROBLEM_PUBLISH_FAIL_STATUS);
+        }
+        problemMapper.updateById(new AfterSaleProblemDO().setId(id)
+                .setStatus(AfterSaleProblemStatusEnum.PUBLISHED.getStatus())
+                .setPublishTime(LocalDateTime.now()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void disableProblem(Long id) {
+        AfterSaleProblemDO problem = validateProblemExists(id);
+        if (!AfterSaleProblemStatusEnum.PUBLISHED.getStatus().equals(problem.getStatus())) {
+            throw exception(PROBLEM_DISABLE_FAIL_STATUS);
+        }
+        problemMapper.updateById(new AfterSaleProblemDO().setId(id)
+                .setStatus(AfterSaleProblemStatusEnum.DISABLED.getStatus()));
+    }
+
+    @Override
+    public List<AfterSaleProblemDO> getReferenceList(AfterSaleProblemPageReqVO pageReqVO) {
+        return problemMapper.selectReferenceList(pageReqVO.getItemId(), pageReqVO.getIssueCategory(),
+                pageReqVO.getIssueType(), pageReqVO.getRootCauseCategory(), pageReqVO.getKeyword());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AfterSaleProblemDO createProblemFromTicket(AfterSaleTicketDO ticket) {
+        // 骞傜瓑锛氬悓涓�宸ュ崟宸叉矇娣�杩囧垯鐩存帴澶嶇敤
+        AfterSaleProblemDO exist = problemMapper.selectBySourceTicketId(ticket.getId());
+        if (exist != null) {
+            return exist;
+        }
+        AfterSaleProblemDO problem = new AfterSaleProblemDO()
+                .setNo(generateNo())
+                .setTitle(ticket.getName())
+                .setSourceTicketId(ticket.getId())
+                .setSourceTicketNo(ticket.getNo())
+                .setCustomerId(ticket.getCustomerId())
+                .setIssueType(ticket.getIssueType())
+                .setIssueCategory(ticket.getIssueCategory())
+                .setSeverityLevel(ticket.getSeverityLevel())
+                .setDescription(ticket.getIssueDescription())
+                .setStatus(AfterSaleProblemStatusEnum.DRAFT.getStatus())
+                .setOwnerUserId(getLoginUserId());
+        // 鍙栧伐鍗曢鏉℃槑缁嗕綔涓虹墿鏂欏揩鐓�(鍏佽鏃犳槑缁嗚)
+        AfterSaleTicketItemDO firstItem = CollUtil.getFirst(ticketItemMapper.selectListByTicketId(ticket.getId()));
+        if (firstItem != null) {
+            problem.setItemId(firstItem.getItemId())
+                    .setItemCode(firstItem.getItemCode())
+                    .setItemName(firstItem.getItemName())
+                    .setBatchId(firstItem.getBatchId())
+                    .setBatchCode(firstItem.getBatchCode());
+        }
+        problemMapper.insert(problem);
+        return problem;
+    }
+
+    // ========== 鍐呴儴鏂规硶 ==========
+
+    private AfterSaleProblemDO validateProblemExists(Long id) {
+        AfterSaleProblemDO problem = problemMapper.selectById(id);
+        if (problem == null) {
+            throw exception(PROBLEM_NOT_EXISTS);
+        }
+        return problem;
+    }
+
+    private String generateNo() {
+        String no = noRedisDAO.generate(AfterSaleNoRedisDAO.PROBLEM_NO_PREFIX);
+        if (problemMapper.selectByNo(no) != null) {
+            throw exception(PROBLEM_NO_EXISTS);
+        }
+        return no;
+    }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
index 41a2163..bc654e7 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
@@ -8,6 +8,8 @@
 import cn.iocoder.yudao.module.aftersales.dal.mysql.repair.AfterSaleRepairMapper;
 import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
 import cn.iocoder.yudao.module.aftersales.enums.AfterSaleRepairStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -67,12 +69,13 @@
         repairMapper.updateById(new AfterSaleRepairDO().setId(id)
                 .setStatus(AfterSaleRepairStatusEnum.COMPLETED.getStatus())
                 .setFinishTime(LocalDateTime.now()));
-        // 鏇存柊宸ュ崟鐘舵��
+        // 鏇存柊宸ュ崟鐘舵�侊細瀹岀粨鍚庤繘鍏ュ緟鍥炶
         AfterSaleTicketDO ticket = ticketMapper.selectById(repair.getTicketId());
         if (ticket != null) {
             ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
-                    .setStatus(cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum.COMPLETED.getStatus())
-                    .setResolvedTime(LocalDateTime.now()));
+                    .setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
+                    .setResolvedTime(LocalDateTime.now())
+                    .setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus()));
         }
     }
 
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 c509cce..bc01293 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
@@ -5,6 +5,7 @@
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
 import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
 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.ticket.AfterSaleTicketItemMapper;
@@ -12,9 +13,11 @@
 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.AfterSaleTicketStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
 import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
 import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
 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 jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
@@ -47,6 +50,9 @@
 
     @Resource
     private AfterSalePermissionService permissionService;
+
+    @Resource
+    private AfterSaleProblemService problemService;
 
     @Resource
     private StorageAttachmentApi storageAttachmentApi;
@@ -146,7 +152,7 @@
         return ticketMapper.selectPage(pageReqVO, userId, pageReqVO.getSceneType(),
                 pageReqVO.getNo(), pageReqVO.getName(),
                 pageReqVO.getCustomerId(), pageReqVO.getSaleOrderId(),
-                pageReqVO.getStatus(), pageReqVO.getIssueType());
+                pageReqVO.getStatus(), pageReqVO.getIssueType(), pageReqVO.getVisitStatus());
     }
 
     @Override
@@ -178,19 +184,31 @@
         AfterSaleTicketDO ticket = validateTicketExists(id);
         validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING,
                 AfterSaleTicketStatusEnum.WAITING_RETURN, AfterSaleTicketStatusEnum.WAITING_REFUND);
+        // 瀹岀粨鍚庤繘鍏ュ緟鍥炶锛岄渶鐧昏婊℃剰鐨勫洖璁垮悗鏂瑰彲褰掓。
         ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                 .setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
-                .setResolvedTime(LocalDateTime.now()));
+                .setResolvedTime(LocalDateTime.now())
+                .setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus()));
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void closeTicket(Long id) {
+        // 1. 鏍¢獙宸ュ崟瀛樺湪涓斿浜庡凡瀹岀粨鐘舵��
         AfterSaleTicketDO ticket = validateTicketExists(id);
         validateTicketStatus(ticket, AfterSaleTicketStatusEnum.COMPLETED);
+        // 2. 鏍¢獙宸插洖璁块�氳繃锛屾湭鍥炶閫氳繃涓嶅厑璁稿綊妗�
+        if (!AfterSaleVisitStatusEnum.PASSED.getStatus().equals(ticket.getVisitStatus())) {
+            throw exception(TICKET_ARCHIVE_FAIL_NOT_VISITED);
+        }
+        // 3. 鑷姩娌夋穩闂(骞傜瓑锛氬悓涓�宸ュ崟浠呮矇娣�涓�鏉�)锛岃繑鍥炶崏绋挎�侀棶棰�
+        AfterSaleProblemDO problem = problemService.createProblemFromTicket(ticket);
+        // 4. 缃綊妗g粓鎬�(鍙)
         ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                 .setStatus(AfterSaleTicketStatusEnum.CLOSED.getStatus())
-                .setClosedTime(LocalDateTime.now()));
+                .setClosedTime(LocalDateTime.now())
+                .setProblemId(problem.getId())
+                .setProblemNo(problem.getNo()));
     }
 
     @Override
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java
new file mode 100644
index 0000000..68a8082
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.aftersales.service.visit;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+/**
+ * 鍞悗鍥炶璁板綍 Service 鎺ュ彛
+ */
+public interface AfterSaleVisitService {
+
+    /**
+     * 鐧昏鍥炶
+     * <p>
+     * 宸ュ崟椤诲浜庡凡瀹岀粨(寰呭洖璁�)鐘舵�併�傜櫥璁版椂鍚屾鎺ㄨ繘宸ュ崟锛�
+     * 婊℃剰涓旀湭澶嶅彂 鈫� 宸ュ崟鍥炶閫氳繃(鍙綊妗�)锛涗笉婊℃剰鎴栧鍙� 鈫� 宸ュ崟鍥為��澶勭悊涓噸鏂板鐞嗐��
+     *
+     * @param createReqVO 鐧昏淇℃伅
+     * @param userId      褰撳墠鐢ㄦ埛缂栧彿(鍥炶浜�)
+     * @return 鍥炶璁板綍缂栧彿
+     */
+    Long createVisit(@Valid AfterSaleVisitCreateReqVO createReqVO, Long userId);
+
+    /**
+     * 鑾峰緱鍥炶璁板綍
+     *
+     * @param id 缂栧彿
+     * @return 鍥炶璁板綍
+     */
+    AfterSaleVisitDO getVisit(Long id);
+
+    /**
+     * 鑾峰緱鏌愬伐鍗曠殑鍥炶璁板綍鍒楄〃
+     *
+     * @param ticketId 宸ュ崟缂栧彿
+     * @return 鍥炶璁板綍鍒楄〃
+     */
+    List<AfterSaleVisitDO> getVisitListByTicketId(Long ticketId);
+
+    /**
+     * 鑾峰緱鍥炶璁板綍鍒嗛〉
+     *
+     * @param pageReqVO 鍒嗛〉鏌ヨ
+     * @return 鍥炶璁板綍鍒嗛〉
+     */
+    PageResult<AfterSaleVisitDO> getVisitPage(AfterSaleVisitPageReqVO pageReqVO);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java
new file mode 100644
index 0000000..f30f75c
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java
@@ -0,0 +1,106 @@
+package cn.iocoder.yudao.module.aftersales.service.visit;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.visit.AfterSaleVisitMapper;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
+import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.TICKET_NOT_EXISTS;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.VISIT_CREATE_FAIL_STATUS;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.VISIT_NOT_EXISTS;
+
+/**
+ * 鍞悗鍥炶璁板綍 Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+@Slf4j
+public class AfterSaleVisitServiceImpl implements AfterSaleVisitService {
+
+    @Resource
+    private AfterSaleVisitMapper visitMapper;
+    @Resource
+    private AfterSaleTicketMapper ticketMapper;
+    @Resource
+    private StorageAttachmentApi storageAttachmentApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createVisit(AfterSaleVisitCreateReqVO createReqVO, Long userId) {
+        // 1. 鏍¢獙宸ュ崟锛氬繀椤诲瓨鍦ㄤ笖澶勪簬宸插畬缁�(寰呭洖璁�)鐘舵��
+        AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getTicketId());
+        if (ticket == null) {
+            throw exception(TICKET_NOT_EXISTS);
+        }
+        if (!AfterSaleTicketStatusEnum.COMPLETED.getStatus().equals(ticket.getStatus())) {
+            throw exception(VISIT_CREATE_FAIL_STATUS);
+        }
+
+        // 2. 鎻掑叆鍥炶璁板綍
+        AfterSaleVisitDO visitDO = BeanUtils.toBean(createReqVO, AfterSaleVisitDO.class)
+                .setVisitorUserId(userId)
+                .setVisitTime(LocalDateTime.now());
+        visitMapper.insert(visitDO);
+
+        // 3. 缁戝畾闄勪欢
+        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
+            storageAttachmentApi.bindAttachments("file", "aftersales_after_sale_visit",
+                    visitDO.getId(), createReqVO.getBlobIds());
+        }
+
+        // 4. 鎺ㄨ繘宸ュ崟鍥炶鐘舵�侊細婊℃剰涓旀湭澶嶅彂 鈫� 閫氳繃锛涗笉婊℃剰鎴栧鍙� 鈫� 鍥為��澶勭悊涓噸鏂板鐞�
+        boolean satisfied = Boolean.TRUE.equals(createReqVO.getSatisfied());
+        boolean recurred = Boolean.TRUE.equals(createReqVO.getRecurred());
+        AfterSaleTicketDO update = new AfterSaleTicketDO().setId(ticket.getId())
+                .setLastVisitTime(visitDO.getVisitTime())
+                .setLastVisitSatisfied(satisfied)
+                .setLastVisitRecurred(recurred);
+        if (satisfied && !recurred) {
+            update.setVisitStatus(AfterSaleVisitStatusEnum.PASSED.getStatus());
+        } else {
+            update.setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus())
+                    .setResolvedTime(null)
+                    .setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus());
+        }
+        ticketMapper.updateById(update);
+        return visitDO.getId();
+    }
+
+    @Override
+    public AfterSaleVisitDO getVisit(Long id) {
+        AfterSaleVisitDO visitDO = visitMapper.selectById(id);
+        if (visitDO == null) {
+            throw exception(VISIT_NOT_EXISTS);
+        }
+        return visitDO;
+    }
+
+    @Override
+    public List<AfterSaleVisitDO> getVisitListByTicketId(Long ticketId) {
+        return visitMapper.selectListByTicketId(ticketId);
+    }
+
+    @Override
+    public PageResult<AfterSaleVisitDO> getVisitPage(AfterSaleVisitPageReqVO pageReqVO) {
+        return visitMapper.selectPage(pageReqVO, pageReqVO.getTicketId(),
+                pageReqVO.getVisitType(), pageReqVO.getSatisfied());
+    }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
index 5691d89..d0e1d22 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -59,6 +59,8 @@
     // ===== 鍞悗 =====
     AFTER_SALE_TICKET("aftersales_after_sale_ticket", "鍞悗宸ュ崟"),
     AFTER_SALE_RETURN("aftersales_after_sale_return", "鍞悗閫�璐х敵璇�"),
+    AFTER_SALE_VISIT("aftersales_after_sale_visit", "鍞悗鍥炶"),
+    AFTER_SALE_PROBLEM("aftersales_after_sale_problem", "鍞悗闂搴�"),
 
     // ===== MES =====
     MES_PD_DOCUMENT("mes_pd_document", "MES 璁捐璧勬枡"),

--
Gitblit v1.9.3