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