From 87d725aa16dcdcdd19f28cd3f3f162e950fd1d2f Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 03 十一月 2026 09:13:20 +0800
Subject: [PATCH] feat(aftersales): 工单助手 App 闭环(签到/转派/升级/处理记录/进度/统计)
---
sql/mysql/cf_aftersales_helper_20261103.sql | 147 +++++++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketTransferReqVO.java | 25 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java | 15
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketEscalateReqVO.java | 24 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java | 82 +++
docs/aftersales_ticket_helper_app_integration.md | 128 ++++++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketLogMapper.java | 38 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketLogRespVO.java | 49 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketStatisticsRespVO.java | 112 +++++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java | 6
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketLogDO.java | 64 +++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSignInReqVO.java | 33 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java | 74 +++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java | 8
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java | 266 ++++++++++++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java | 1
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketLogActionEnum.java | 50 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketHandleReqVO.java | 23 +
18 files changed, 1,126 insertions(+), 19 deletions(-)
diff --git a/docs/aftersales_ticket_helper_app_integration.md b/docs/aftersales_ticket_helper_app_integration.md
new file mode 100644
index 0000000..899f628
--- /dev/null
+++ b/docs/aftersales_ticket_helper_app_integration.md
@@ -0,0 +1,128 @@
+# 宸ュ崟鍔╂墜锛圓pp锛夋帴鍙h仈璋冩柟妗�
+
+## 娑夊強椤甸潰
+
+- App 宸ヤ綔鍙般�屽伐鍗曞姪鎵嬨�嶅垎缁�
+- App 宸ュ崟涓婃姤锛堟柊澧� / 缂栬緫鍞悗宸ュ崟锛�
+- App 宸ュ崟鍙楃悊锛堝敭鍚庡伐鍗曞垪琛級
+- App 鍞悗宸ュ崟璇︽儏锛堣繘搴︺�佺鍒般�佽浆娲俱�佸崌绾с�佸鐞嗚褰曪級
+- App 宸ュ崟鍒嗘瀽锛堢粺璁$湅鏉匡級
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. 宸ュ崟涓婃姤 鈫� 宸ュ崟鍙楃悊锛氫笂鎶ュ悗宸ュ崟鐘舵�佷负銆屽鐞嗕腑銆嶏紝鑷姩鍐欏叆涓�鏉� `CREATE` 杩涘害璁板綍銆�
+2. 宸ュ崟鍙楃悊 鈫� 鍞悗宸ュ崟璇︽儏锛氬垪琛ㄧ偣鍏ヨ鎯咃紝鎸� `ticketId` 鎷夊彇杩涘害璁板綍銆佸洖璁胯褰曚笌闄勪欢銆�
+3. 璇︽儏 鈫� 鐜板満绛惧埌锛欰pp 鍙栧綋鍓嶅畾浣嶏紙缁忓害 / 绾害锛夊苟濉啓鐜板満鍦板潃锛岄娆$鍒板洖鍐欏伐鍗� `signInTime` / `signInAddress`銆�
+4. 璇︽儏 鈫� 杞淳 / 鍗囩骇 / 澶勭悊璁板綍锛氬潎鍐欏叆 `after_sale_ticket_log`锛岃繘搴︽椂闂寸嚎瀹炴椂鍒锋柊銆�
+5. 璇︽儏 鈫� 瀹岀粨 鈫� 鍥炶锛氬洖璁跨櫥璁版垚鍔熷悗鍐� `VISIT` 璁板綍锛涗笉婊℃剰鎴栧鍙戜細閫�鍥炪�屽鐞嗕腑銆嶃��
+6. 宸ュ崟鍒嗘瀽锛氭寜鏁版嵁鏉冮檺鍦烘櫙锛堟垜璐熻矗鐨� / 鎴戝弬涓庣殑 / 涓嬪睘璐熻矗鐨� / 鍏ㄩ儴锛夋眹鎬诲伐鍗曠姸鎬併�侀棶棰樼被鍨嬨�佸鐞嗕汉宸ヤ綔閲忋��
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /aftersales/ticket/sign-in | 鐜板満绛惧埌 |
+| POST | /aftersales/ticket/transfer | 杞淳宸ュ崟 |
+| POST | /aftersales/ticket/escalate | 鍗囩骇宸ュ崟 |
+| POST | /aftersales/ticket/handle | 鏂板澶勭悊璁板綍 |
+| GET | /aftersales/ticket/log-list | 宸ュ崟鎿嶄綔 / 杩涘害璁板綍鍒楄〃 |
+| GET | /aftersales/ticket/statistics | 宸ュ崟鍔╂墜缁熻 |
+
+鏉冮檺鐮侊細绛惧埌銆佽浆娲俱�佸崌绾с�佸鐞嗚褰曠粺涓�浣跨敤 `aftersales:ticket:update`锛涜繘搴﹁褰曚笌缁熻浣跨敤鐧诲綍鎬侊紝鏃犻澶栨寜閽潈闄愩��
+
+**璇锋眰鍙傛暟锛�**
+
+绛惧埌 `POST /aftersales/ticket/sign-in`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿 |
+| longitude | BigDecimal | 鍚� | 缁忓害锛孉pp 瀹氫綅澶辫触鏃跺厑璁镐负绌� |
+| latitude | BigDecimal | 鍚� | 绾害 |
+| address | String | 鍚� | 鐜板満鍦板潃锛堝缓璁鎴风蹇呭~锛屼究浜庤拷婧級 |
+| content | String | 鍚� | 绛惧埌璇存槑 |
+
+杞淳 `POST /aftersales/ticket/transfer`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿 |
+| handlerUserId | Long | 鏄� | 鏂板鐞嗕汉锛屼笉鑳戒笌褰撳墠澶勭悊浜虹浉鍚� |
+| reason | String | 鍚� | 杞淳鍘熷洜 |
+
+鍗囩骇 `POST /aftersales/ticket/escalate`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿 |
+| severityLevel | Integer | 鍚� | 鍗囩骇鍚庣殑涓ラ噸绋嬪害锛�0 杞诲井 / 1 涓�鑸� / 2 涓ラ噸 / 3 鑷村懡锛夛紝涓嶄紶鍒欒嚜鍔� +1锛屾渶楂� 3 |
+| reason | String | 鍚� | 鍗囩骇鍘熷洜 |
+
+澶勭悊璁板綍 `POST /aftersales/ticket/handle`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿 |
+| content | String | 鏄� | 澶勭悊鍔ㄤ綔涓庣粨鏋� |
+
+杩涘害璁板綍 `GET /aftersales/ticket/log-list?ticketId=1`
+
+缁熻 `GET /aftersales/ticket/statistics?sceneType=4`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| sceneType | Integer | 鍚� | 1 鎴戣礋璐g殑 / 2 鎴戝弬涓庣殑 / 3 涓嬪睘璐熻矗鐨� / 4 鍏ㄩ儴锛岄粯璁� 4 |
+
+**鍝嶅簲瀛楁锛�**
+
+杩涘害璁板綍椤癸細
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| actionType | Integer | 1 涓婃姤 / 2 娲惧崟 / 3 绛惧埌 / 4 闂鍒ゅ畾 / 5 杞淳 / 6 鍗囩骇 / 7 澶勭悊璁板綍 / 8 瀹岀粨 / 9 鍥炶 / 10 褰掓。 / 11 鍙栨秷 |
+| actionName | String | 鎿嶄綔鍚嶇О锛堝悗绔凡缈昏瘧锛� |
+| content | String | 鎿嶄綔鍐呭 |
+| operatorUserName | String | 鎿嶄綔浜烘樀绉� |
+| longitude / latitude | BigDecimal | 绛惧埌鍧愭爣 |
+| address | String | 绛惧埌鍦板潃 |
+| createTime | LocalDateTime | 鎿嶄綔鏃堕棿 |
+
+缁熻鍝嶅簲锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| totalCount / processingCount / waitingReturnCount / waitingRefundCount | Long | 鎬婚噺涓庡悇闃舵鏁伴噺 |
+| completedCount / closedCount / canceledCount | Long | 宸插畬缁� / 宸插叧闂� / 宸插彇娑� |
+| todayCreateCount / todayResolveCount | Long | 浠婃棩涓婃姤 / 浠婃棩瀹岀粨 |
+| signInCount / escalatedCount | Long | 绛惧埌娆℃暟 / 鍗囩骇杩囩殑宸ュ崟鏁� |
+| avgResolveHours | BigDecimal | 骞冲潎澶勭悊鏃堕暱锛堝皬鏃讹級 |
+| satisfactionRate | BigDecimal | 鍥炶婊℃剰搴︼紙%锛� |
+| statusCounts | Array | `{ status, statusName, count }` |
+| issueTypeCounts | Array | `{ issueType, issueTypeName, count }` |
+| handlerWorkloads | Array | `{ handlerUserId, handlerUserName, processingCount, totalCount }` |
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| signInTime / signInAddress | 宸ュ崟璇︽儏 | 鏄剧ず銆屾湭绛惧埌銆嶆椂鎻愪緵涓�涓幇鍦虹鍒版寜閽� |
+| 杩涘害璁板綍 | 宸ュ崟璇︽儏銆屽鐞嗚繘搴︺�� | 鎸夋椂闂存搴忓睍绀猴紝绛惧埌璁板綍闄勫甫瀹氫綅 |
+| actionName | 杩涘害鏃堕棿绾挎爣棰� | 鐩存帴浣跨敤鍚庣杩斿洖鍊硷紝涓嶅湪鍓嶇閲嶅缁存姢鏋氫妇 |
+| 缁熻鐪嬫澘 | 宸ュ崟鍒嗘瀽 | 鍗$墖鍒诲害 + 妯悜鍗犳瘮鏉★紝澶勭悊浜哄伐浣滈噺鎸夊鐞嗕腑 / 绱灞曠ず |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 鐜板満绛惧埌 | 浠呫�屽鐞嗕腑銆嶅伐鍗曞彲绛惧埌锛涢娆$鍒板洖鍐欏伐鍗曪紝鍚庣画绛惧埌浠呰拷鍔犺褰� |
+| 杞淳 | 浠呫�屽鐞嗕腑銆嶅彲杞淳锛涙柊澶勭悊浜轰笌褰撳墠澶勭悊浜虹浉鍚屼細杩斿洖鏄庣‘閿欒 |
+| 鍗囩骇 | 浠呫�屽鐞嗕腑銆嶅伐鍗曞彲鍗囩骇锛涗弗閲嶇▼搴﹀彧鍗囦笉闄� |
+| 澶勭悊璁板綍 | 澶勭悊涓�佸緟閫�璐с�佸緟閫�娆剧姸鎬佸彲杩藉姞 |
+| 绂荤嚎涓婃姤 | 鏃犵綉缁滄椂宸ュ崟鏆傚瓨鏈湴锛岃繘鍏ュ伐鍗曞垪琛ㄤ笖鏈夌綉鏃惰嚜鍔ㄨˉ浼狅紱涓氬姟鏍¢獙澶辫触鐨勭绾垮崟浼氳璺宠繃 |
+
+## 娉ㄦ剰浜嬮」
+
+- 绛惧埌瀹氫綅澶辫触涓嶉樆鏂鍒帮細鍦板潃浠嶅缓璁繀濉紝渚夸簬杩芥函銆�
+- 杩涘害璁板綍渚濊禆鐧诲綍鐢ㄦ埛锛屾搷浣滀汉鐢卞悗绔粠涓婁笅鏂囪幏鍙栵紝鍓嶇涓嶈浼� `operatorUserId`銆�
+- 缁熻鎺ュ彛鎸夋暟鎹潈闄愬満鏅繃婊わ紝`sceneType` 涓庡伐鍗曞垪琛ㄤ繚鎸佷竴鑷淬��
+- 绂荤嚎闃熷垪琛ヤ紶浣跨敤鍒涘缓鏃剁殑 token锛泃oken 澶辨晥鏃堕渶鐢ㄦ埛閲嶆柊鐧诲綍鍚庡啀琛ヤ紶銆�
diff --git a/sql/mysql/cf_aftersales_helper_20261103.sql b/sql/mysql/cf_aftersales_helper_20261103.sql
new file mode 100644
index 0000000..1f98846
--- /dev/null
+++ b/sql/mysql/cf_aftersales_helper_20261103.sql
@@ -0,0 +1,147 @@
+-- =====================================================================
+-- 璇氬彂浜旈噾 路 宸ュ崟鍔╂墜锛圓pp锛夊悗绔厤濂楄剼鏈�
+-- 渚濇嵁銆婇檮浠朵竴锛氭妧鏈柟妗�-璇氬彂浜旈噾锛�2026锛夈�嬬锛堝叓锛夌珷銆屽伐鍗曞姪鎵嬨��
+-- 鍐呭锛�
+-- 1. after_sale_ticket 澧炲姞鐜板満绛惧埌浣嶏紙sign_in_time / sign_in_address锛�
+-- 2. 鏂板缓 after_sale_ticket_log 宸ュ崟鎿嶄綔/杩涘害璁板綍琛紙杩涘害璺熻釜銆佸叏鐢熷懡鍛ㄦ湡杩芥函锛�
+-- 3. 琛ャ�屽伐鍗曞姪鎵嬨�嶈彍鍗曚笌鎸夐挳鏉冮檺锛堣彍鍗� visible=0锛氫粎 App 浣跨敤锛屼笉鏀瑰彉 Web 渚ц竟鏍忥級
+-- 4. 缁欓攢鍞�/鐢熶骇绠$悊鍛�/缁忕悊瑙掕壊鎺堟潈
+-- 閫傜敤搴擄細璇氬彂浜旈噾鏈湴搴� CFWJ锛堝崟绉熸埛锛岃剼鏈笉鍚� tenant_id锛�
+-- 骞傜瓑锛氬彲閲嶅鎵ц
+-- =====================================================================
+
+SET NAMES utf8mb4;
+
+-- ==================== 1. after_sale_ticket 鐜板満绛惧埌浣� ====================
+SET @exists_sign_in_time = (SELECT COUNT(*) FROM information_schema.columns
+ WHERE table_schema = DATABASE() AND table_name = 'after_sale_ticket' AND column_name = 'sign_in_time');
+SET @sql = IF(@exists_sign_in_time = 0,
+ 'ALTER TABLE `after_sale_ticket` ADD COLUMN `sign_in_time` datetime DEFAULT NULL COMMENT ''棣栨鐜板満绛惧埌鏃堕棿'' AFTER `repair_id`',
+ 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @exists_sign_in_address = (SELECT COUNT(*) FROM information_schema.columns
+ WHERE table_schema = DATABASE() AND table_name = 'after_sale_ticket' AND column_name = 'sign_in_address');
+SET @sql = IF(@exists_sign_in_address = 0,
+ 'ALTER TABLE `after_sale_ticket` ADD COLUMN `sign_in_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''棣栨鐜板満绛惧埌鍦板潃'' AFTER `sign_in_time`',
+ 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== 2. 宸ュ崟鎿嶄綔/杩涘害璁板綍琛� ====================
+CREATE TABLE IF NOT EXISTS `after_sale_ticket_log` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '璁板綍缂栧彿',
+ `ticket_id` bigint NOT NULL COMMENT '宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id',
+ `action_type` tinyint NOT NULL COMMENT '鎿嶄綔绫诲瀷: 1-涓婃姤 2-娲惧崟 3-绛惧埌 4-闂鍒ゅ畾 5-杞淳 6-鍗囩骇 7-澶勭悊璁板綍 8-瀹岀粨 9-鍥炶 10-褰掓。 11-鍙栨秷',
+ `content` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鎿嶄綔鍐呭/璇存槑',
+ `operator_user_id` bigint DEFAULT NULL COMMENT '鎿嶄綔浜虹敤鎴风紪鍙�',
+ `longitude` decimal(12,8) DEFAULT NULL COMMENT '缁忓害锛堢幇鍦虹鍒版椂涓婃姤锛�',
+ `latitude` decimal(12,8) DEFAULT NULL COMMENT '绾害锛堢幇鍦虹鍒版椂涓婃姤锛�',
+ `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '瀹氫綅鍦板潃锛堢幇鍦虹鍒版椂涓婃姤锛�',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_ticket_id` (`ticket_id`) USING BTREE,
+ KEY `idx_action_type` (`action_type`) USING BTREE,
+ KEY `idx_create_time` (`create_time`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='鍞悗宸ュ崟鎿嶄綔/杩涘害璁板綍琛�';
+
+-- ==================== 3. 宸ュ崟鍔╂墜鑿滃崟锛圓pp 浣跨敤锛學eb 渚т笉鏄剧ず锛� ====================
+START TRANSACTION;
+
+-- 3.1 涓�绾х洰褰�
+INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT '宸ュ崟鍔╂墜', '', 1, 20, 0, 'aftersales-helper', 'ep:headset', NULL, NULL,
+ 0, b'0', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = 0 AND `name` = '宸ュ崟鍔╂墜');
+SET @helper_id = (SELECT id FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = 0 AND `name` = '宸ュ崟鍔╂墜' ORDER BY id LIMIT 1);
+
+-- 3.2 宸ュ崟鍙楃悊椤甸潰
+INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT '宸ュ崟鍙楃悊', '', 2, 1, @helper_id, 'ticket', 'ep:document', 'aftersales/ticket/index', NULL,
+ 0, b'0', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/ticket/index');
+SET @ticket_menu_id = (SELECT id FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/ticket/index' ORDER BY id LIMIT 1);
+
+-- 3.3 閫�璐х敵璇烽〉闈�
+INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT '閫�璐х敵璇�', '', 2, 2, @helper_id, 'return', 'ep:refresh-left', 'aftersales/return/index', NULL,
+ 0, b'0', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/return/index');
+SET @return_menu_id = (SELECT id FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/return/index' ORDER BY id LIMIT 1);
+
+-- 3.4 闂搴撻〉闈�
+INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT '闂搴�', '', 2, 3, @helper_id, 'problem', 'ep:notebook', 'aftersales/problem/index', NULL,
+ 0, b'0', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/problem/index');
+SET @problem_menu_id = (SELECT id FROM `system_menu` WHERE `deleted` = b'0' AND `parent_id` = @helper_id AND `component` = 'aftersales/problem/index' ORDER BY id LIMIT 1);
+
+-- 3.5 鎸夐挳鏉冮檺锛堟寜 permission 骞傜瓑锛�
+INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT t.`name`, t.`permission`, 3, t.`sort`, t.`parent_id`, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (
+ SELECT '宸ュ崟鏌ヨ' AS `name`, 'aftersales:ticket:query' AS `permission`, 1 AS `sort`, @ticket_menu_id AS `parent_id`
+ UNION ALL SELECT '宸ュ崟涓婃姤', 'aftersales:ticket:create', 2, @ticket_menu_id
+ UNION ALL SELECT '宸ュ崟澶勭悊', 'aftersales:ticket:update', 3, @ticket_menu_id
+ UNION ALL SELECT '宸ュ崟鍒犻櫎', 'aftersales:ticket:delete', 4, @ticket_menu_id
+ UNION ALL SELECT '宸ュ崟鍥炶', 'aftersales:visit:create', 5, @ticket_menu_id
+ UNION ALL SELECT '閫�璐ф煡璇�', 'aftersales:return:query', 1, @return_menu_id
+ UNION ALL SELECT '閫�璐х敵璇�', 'aftersales:return:create', 2, @return_menu_id
+ UNION ALL SELECT '閫�璐у鏍�', 'aftersales:return:update', 3, @return_menu_id
+ UNION ALL SELECT '闂鏌ヨ', 'aftersales:problem:query', 1, @problem_menu_id
+ UNION ALL SELECT '闂鏂板', 'aftersales:problem:create', 2, @problem_menu_id
+ UNION ALL SELECT '闂淇敼', 'aftersales:problem:update', 3, @problem_menu_id
+ UNION ALL SELECT '闂鍒犻櫎', 'aftersales:problem:delete', 4, @problem_menu_id
+ UNION ALL SELECT '闂鍙戝竷', 'aftersales:problem:publish', 5, @problem_menu_id
+ UNION ALL SELECT '闂鍋滅敤', 'aftersales:problem:disable', 6, @problem_menu_id
+) t
+WHERE NOT EXISTS (SELECT 1 FROM `system_menu` m WHERE m.`deleted` = b'0' AND m.`permission` = t.`permission`);
+
+-- 3.6 瑙掕壊鎺堟潈锛氶攢鍞�佺敓浜х鐞嗗憳銆佺粡鐞嗭紙瓒呯骇绠$悊鍛樼敱浠g爜鐩撮�氾紝鏃犻渶琛岋級
+CREATE TEMPORARY TABLE IF NOT EXISTS `tmp_helper_role` (`role_id` bigint NOT NULL PRIMARY KEY);
+TRUNCATE TABLE `tmp_helper_role`;
+INSERT INTO `tmp_helper_role` (`role_id`) VALUES (165), (166), (168);
+
+INSERT INTO `system_role_menu` (`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT r.`role_id`, m.`id`, '1', NOW(), '1', NOW(), b'0'
+FROM `tmp_helper_role` r
+JOIN `system_menu` m ON m.`deleted` = b'0' AND (
+ m.`id` IN (@helper_id, @ticket_menu_id, @return_menu_id, @problem_menu_id)
+ OR m.`permission` LIKE 'aftersales:%'
+)
+WHERE NOT EXISTS (
+ SELECT 1 FROM `system_role_menu` rm
+ WHERE rm.`role_id` = r.`role_id` AND rm.`menu_id` = m.`id` AND rm.`deleted` = b'0'
+);
+
+DROP TEMPORARY TABLE IF EXISTS `tmp_helper_role`;
+
+COMMIT;
+
+-- ==================== 4. 楠岃瘉 ====================
+-- 1) 宸ュ崟鍔╂墜鑿滃崟鏍�
+SELECT id, name, permission, type, parent_id, visible, component
+FROM `system_menu`
+WHERE `deleted` = b'0' AND (name = '宸ュ崟鍔╂墜' OR parent_id = @helper_id
+ OR permission LIKE 'aftersales:%')
+ORDER BY parent_id, type, sort;
+-- 2) 瑙掕壊鎺堟潈鏁伴噺
+SELECT rm.role_id, COUNT(*) AS menu_count
+FROM `system_role_menu` rm
+JOIN `system_menu` m ON m.id = rm.menu_id AND m.deleted = b'0'
+WHERE rm.deleted = b'0' AND (m.name = '宸ュ崟鍔╂墜' OR m.permission LIKE 'aftersales:%')
+GROUP BY rm.role_id;
+-- 3) 杩涘害璁板綍琛�
+SELECT COUNT(*) AS log_count FROM `after_sale_ticket_log`;
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
index 6600104..35a0321 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
@@ -5,11 +5,19 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketEscalateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketHandleReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketLogRespVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketRespVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSignInReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketStatisticsRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketTransferReqVO;
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.dataobject.ticket.AfterSaleTicketLogDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketLogActionEnum;
import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
@@ -97,7 +105,7 @@
@PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
public CommonResult<Boolean> assignTicket(@RequestParam("id") Long id,
@RequestParam("handlerUserId") Long handlerUserId) {
- ticketService.assignTicket(id, handlerUserId);
+ ticketService.assignTicket(id, handlerUserId, getLoginUserId());
return success(true);
}
@@ -108,7 +116,7 @@
@RequestParam("issueType") Integer issueType,
@RequestParam(value = "issueCategory", required = false) String issueCategory,
@RequestParam(value = "severityLevel", required = false) Integer severityLevel) {
- ticketService.classifyTicket(id, issueType, issueCategory, severityLevel);
+ ticketService.classifyTicket(id, issueType, issueCategory, severityLevel, getLoginUserId());
return success(true);
}
@@ -116,7 +124,7 @@
@Operation(summary = "瀹岀粨宸ュ崟")
@PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
public CommonResult<Boolean> resolveTicket(@RequestParam("id") Long id) {
- ticketService.resolveTicket(id);
+ ticketService.resolveTicket(id, getLoginUserId());
return success(true);
}
@@ -124,7 +132,7 @@
@Operation(summary = "鍏抽棴宸ュ崟")
@PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
public CommonResult<Boolean> closeTicket(@RequestParam("id") Long id) {
- ticketService.closeTicket(id);
+ ticketService.closeTicket(id, getLoginUserId());
return success(true);
}
@@ -132,10 +140,74 @@
@Operation(summary = "鍙栨秷宸ュ崟")
@PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
public CommonResult<Boolean> cancelTicket(@RequestParam("id") Long id) {
- ticketService.cancelTicket(id);
+ ticketService.cancelTicket(id, getLoginUserId());
return success(true);
}
+ @PostMapping("/sign-in")
+ @Operation(summary = "鐜板満绛惧埌")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> signInTicket(@Valid @RequestBody AfterSaleTicketSignInReqVO reqVO) {
+ ticketService.signInTicket(reqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @PostMapping("/transfer")
+ @Operation(summary = "杞淳宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> transferTicket(@Valid @RequestBody AfterSaleTicketTransferReqVO reqVO) {
+ ticketService.transferTicket(reqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @PostMapping("/escalate")
+ @Operation(summary = "鍗囩骇宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> escalateTicket(@Valid @RequestBody AfterSaleTicketEscalateReqVO reqVO) {
+ ticketService.escalateTicket(reqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @PostMapping("/handle")
+ @Operation(summary = "鏂板澶勭悊璁板綍")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> addHandleRecord(@Valid @RequestBody AfterSaleTicketHandleReqVO reqVO) {
+ ticketService.addHandleRecord(reqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/log-list")
+ @Operation(summary = "鑾峰緱宸ュ崟鎿嶄綔/杩涘害璁板綍鍒楄〃")
+ @Parameter(name = "ticketId", description = "宸ュ崟缂栧彿", required = true, example = "1")
+ public CommonResult<List<AfterSaleTicketLogRespVO>> getTicketLogList(@RequestParam("ticketId") Long ticketId) {
+ return success(buildLogRespList(ticketService.getTicketLogList(ticketId)));
+ }
+
+ @GetMapping("/statistics")
+ @Operation(summary = "鑾峰緱宸ュ崟鍔╂墜缁熻")
+ @Parameter(name = "sceneType", description = "鍦烘櫙绫诲瀷: 1-鎴戣礋璐g殑 2-鎴戝弬涓庣殑 3-涓嬪睘璐熻矗鐨� 4-鍏ㄩ儴", example = "4")
+ public CommonResult<AfterSaleTicketStatisticsRespVO> getTicketStatistics(
+ @RequestParam(value = "sceneType", required = false) Integer sceneType) {
+ return success(ticketService.getTicketStatistics(sceneType, getLoginUserId()));
+ }
+
+ // ========== 鍐呴儴鏂规硶锛氭嫾瑁呰繘搴﹁褰� ==========
+
+ private List<AfterSaleTicketLogRespVO> buildLogRespList(List<AfterSaleTicketLogDO> logList) {
+ if (CollUtil.isEmpty(logList)) {
+ return Collections.emptyList();
+ }
+ Set<Long> userIds = convertSet(logList, AfterSaleTicketLogDO::getOperatorUserId);
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+ return BeanUtils.toBean(logList, AfterSaleTicketLogRespVO.class, vo -> {
+ vo.setActionName(AfterSaleTicketLogActionEnum.getName(vo.getActionType()));
+ AdminUserRespDTO user = userMap.get(vo.getOperatorUserId());
+ if (user != null) {
+ vo.setOperatorUserName(user.getNickname());
+ }
+ });
+ }
+
// ========== 鍐呴儴鏂规硶锛氭壒閲忔嫾瑁呭叧鑱斿悕绉� ==========
private List<AfterSaleTicketRespVO> buildTicketDetailList(List<AfterSaleTicketDO> ticketList) {
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketEscalateReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketEscalateReqVO.java
new file mode 100644
index 0000000..5bd8118
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketEscalateReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 鍞悗宸ュ崟 - 鍗囩骇 Request VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 鍗囩骇 Request VO")
+@Data
+public class AfterSaleTicketEscalateReqVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "宸ュ崟缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鍗囩骇鍚庝弗閲嶇▼搴�: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡", example = "2")
+ private Integer severityLevel;
+
+ @Schema(description = "鍗囩骇鍘熷洜", example = "鐜板満鎺掓煡鍙戠幇涓绘澘鐑ф瘉锛屽奖鍝嶆暣鏉′骇绾�")
+ private String reason;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketHandleReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketHandleReqVO.java
new file mode 100644
index 0000000..4870243
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketHandleReqVO.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 鍞悗宸ュ崟 - 澶勭悊璁板綍 Request VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 澶勭悊璁板綍 Request VO")
+@Data
+public class AfterSaleTicketHandleReqVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "宸ュ崟缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "澶勭悊鍐呭", requiredMode = Schema.RequiredMode.REQUIRED, example = "宸叉洿鎹㈡崯鍧忕殑杞存壙骞跺畬鎴愯瘯鏈�")
+ @NotBlank(message = "澶勭悊鍐呭涓嶈兘涓虹┖")
+ private String content;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketLogRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketLogRespVO.java
new file mode 100644
index 0000000..ff1ed2c
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketLogRespVO.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗宸ュ崟鎿嶄綔/杩涘害璁板綍 Response VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 鎿嶄綔/杩涘害璁板綍 Response VO")
+@Data
+public class AfterSaleTicketLogRespVO {
+
+ @Schema(description = "璁板綍缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "宸ュ崟缂栧彿", example = "1")
+ private Long ticketId;
+
+ @Schema(description = "鎿嶄綔绫诲瀷: 1-涓婃姤 2-娲惧崟 3-绛惧埌 4-闂鍒ゅ畾 5-杞淳 6-鍗囩骇 7-澶勭悊璁板綍 8-瀹岀粨 9-鍥炶 10-褰掓。 11-鍙栨秷", example = "3")
+ private Integer actionType;
+
+ @Schema(description = "鎿嶄綔绫诲瀷鍚嶇О", example = "鐜板満绛惧埌")
+ private String actionName;
+
+ @Schema(description = "鎿嶄綔鍐呭", example = "宸插埌杈惧鎴风幇鍦猴紝璁惧杩愯寮傚搷")
+ private String content;
+
+ @Schema(description = "鎿嶄綔浜虹敤鎴风紪鍙�", example = "1")
+ private Long operatorUserId;
+
+ @Schema(description = "鎿嶄綔浜哄悕绉�", example = "寮犲伐")
+ private String operatorUserName;
+
+ @Schema(description = "缁忓害", example = "118.180000")
+ private BigDecimal longitude;
+
+ @Schema(description = "绾害", example = "39.630000")
+ private BigDecimal latitude;
+
+ @Schema(description = "瀹氫綅鍦板潃", example = "娌冲寳鐪佸攼灞卞競婊﹀崡鍘縓X璺疿X鍙�")
+ private String address;
+
+ @Schema(description = "鎿嶄綔鏃堕棿")
+ private LocalDateTime createTime;
+
+}
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 dc16302..0182c08 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
@@ -81,6 +81,12 @@
@Schema(description = "鍏宠仈缁翠慨璁板綍缂栧彿", example = "1")
private Long repairId;
+ @Schema(description = "棣栨鐜板満绛惧埌鏃堕棿")
+ private LocalDateTime signInTime;
+
+ @Schema(description = "棣栨鐜板満绛惧埌鍦板潃")
+ private String signInAddress;
+
@Schema(description = "瑙e喅鏃堕棿")
private LocalDateTime resolvedTime;
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSignInReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSignInReqVO.java
new file mode 100644
index 0000000..7bd2e14
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSignInReqVO.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 鍞悗宸ュ崟 - 鐜板満绛惧埌 Request VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 鐜板満绛惧埌 Request VO")
+@Data
+public class AfterSaleTicketSignInReqVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "宸ュ崟缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "缁忓害", example = "118.180000")
+ private BigDecimal longitude;
+
+ @Schema(description = "绾害", example = "39.630000")
+ private BigDecimal latitude;
+
+ @Schema(description = "瀹氫綅鍦板潃", example = "娌冲寳鐪佸攼灞卞競婊﹀崡鍘縓X璺疿X鍙�")
+ private String address;
+
+ @Schema(description = "绛惧埌璇存槑", example = "宸插埌杈惧鎴风幇鍦�")
+ private String content;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketStatisticsRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketStatisticsRespVO.java
new file mode 100644
index 0000000..967aee2
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketStatisticsRespVO.java
@@ -0,0 +1,112 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟 - 宸ュ崟鍔╂墜缁熻 Response VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 宸ュ崟鍔╂墜缁熻 Response VO")
+@Data
+public class AfterSaleTicketStatisticsRespVO {
+
+ @Schema(description = "宸ュ崟鎬绘暟", example = "42")
+ private Long totalCount;
+
+ @Schema(description = "澶勭悊涓暟閲�", example = "8")
+ private Long processingCount;
+
+ @Schema(description = "寰呴��璐ф暟閲�", example = "2")
+ private Long waitingReturnCount;
+
+ @Schema(description = "寰呴��娆�/鎹㈣揣鏁伴噺", example = "1")
+ private Long waitingRefundCount;
+
+ @Schema(description = "宸插畬缁�(寰呭洖璁�)鏁伴噺", example = "5")
+ private Long completedCount;
+
+ @Schema(description = "宸插叧闂�(褰掓。)鏁伴噺", example = "20")
+ private Long closedCount;
+
+ @Schema(description = "宸插彇娑堟暟閲�", example = "1")
+ private Long canceledCount;
+
+ @Schema(description = "浠婃棩鏂板鏁伴噺", example = "3")
+ private Long todayCreateCount;
+
+ @Schema(description = "浠婃棩瀹岀粨鏁伴噺", example = "2")
+ private Long todayResolveCount;
+
+ @Schema(description = "宸茬幇鍦虹鍒板伐鍗曟暟閲�", example = "15")
+ private Long signInCount;
+
+ @Schema(description = "琚崌绾ц繃鐨勫伐鍗曟暟閲�", example = "4")
+ private Long escalatedCount;
+
+ @Schema(description = "骞冲潎瀹岀粨鑰楁椂锛堝皬鏃讹級", example = "6.5")
+ private BigDecimal avgResolveHours;
+
+ @Schema(description = "鍥炶婊℃剰鐜囷紙鐧惧垎姣旓紝0-100锛�", example = "96.7")
+ private BigDecimal satisfactionRate;
+
+ @Schema(description = "鎸夌姸鎬佺粺璁�")
+ private List<StatusCount> statusCounts;
+
+ @Schema(description = "鎸夐棶棰樼被鍨嬬粺璁�")
+ private List<IssueTypeCount> issueTypeCounts;
+
+ @Schema(description = "鎸夊鐞嗕汉缁熻宸ヤ綔閲�")
+ private List<HandlerWorkload> handlerWorkloads;
+
+ @Schema(description = "鐘舵�佺粺璁�")
+ @Data
+ public static class StatusCount {
+
+ @Schema(description = "鐘舵��", example = "10")
+ private Integer status;
+
+ @Schema(description = "鐘舵�佸悕绉�", example = "澶勭悊涓�")
+ private String statusName;
+
+ @Schema(description = "鏁伴噺", example = "8")
+ private Long count;
+
+ }
+
+ @Schema(description = "闂绫诲瀷缁熻")
+ @Data
+ public static class IssueTypeCount {
+
+ @Schema(description = "闂绫诲瀷", example = "2")
+ private Integer issueType;
+
+ @Schema(description = "闂绫诲瀷鍚嶇О", example = "缁翠慨闂")
+ private String issueTypeName;
+
+ @Schema(description = "鏁伴噺", example = "12")
+ private Long count;
+
+ }
+
+ @Schema(description = "澶勭悊浜哄伐浣滈噺")
+ @Data
+ public static class HandlerWorkload {
+
+ @Schema(description = "澶勭悊浜虹敤鎴风紪鍙�", example = "2")
+ private Long handlerUserId;
+
+ @Schema(description = "澶勭悊浜哄悕绉�", example = "寮犲伐")
+ private String handlerUserName;
+
+ @Schema(description = "澶勭悊涓暟閲�", example = "3")
+ private Long processingCount;
+
+ @Schema(description = "绱澶勭悊鏁伴噺", example = "12")
+ private Long totalCount;
+
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketTransferReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketTransferReqVO.java
new file mode 100644
index 0000000..3cd0b4b
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketTransferReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 鍞悗宸ュ崟 - 杞淳 Request VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 杞淳 Request VO")
+@Data
+public class AfterSaleTicketTransferReqVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "宸ュ崟缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鏂板鐞嗕汉鐢ㄦ埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+ @NotNull(message = "鏂板鐞嗕汉涓嶈兘涓虹┖")
+ private Long handlerUserId;
+
+ @Schema(description = "杞淳鍘熷洜", example = "璇ラ棶棰橀渶瑕佺數姘斿伐绋嬪笀澶勭悊")
+ private String reason;
+
+}
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 4f71c3e..a688489 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
@@ -101,6 +101,14 @@
*/
private Long repairId;
/**
+ * 棣栨鐜板満绛惧埌鏃堕棿
+ */
+ private LocalDateTime signInTime;
+ /**
+ * 棣栨鐜板満绛惧埌鍦板潃
+ */
+ private String signInAddress;
+ /**
* 瑙e喅鏃堕棿
*/
private LocalDateTime resolvedTime;
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketLogDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketLogDO.java
new file mode 100644
index 0000000..385341e
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketLogDO.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketLogActionEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * 鍞悗宸ュ崟鎿嶄綔/杩涘害璁板綍 DO
+ * <p>
+ * 宸ュ崟鍔╂墜 App 鐨勩�岃繘搴﹁窡韪�嶆暟鎹潵婧愶紝瀹屾暣璁板綍宸ュ崟鍏ㄧ敓鍛藉懆鏈熸搷浣�
+ * 锛堟搷浣滄椂闂淬�佹搷浣滀汉銆佸姩浣溿�佺幇鍦轰綅缃級锛屼繚璇佸敭鍚庤繃绋嬪彲杩芥函銆�
+ */
+@TableName("after_sale_ticket_log")
+@KeySequence("after_sale_ticket_log_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleTicketLogDO extends BaseDO {
+
+ /**
+ * 璁板綍缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+ */
+ private Long ticketId;
+ /**
+ * 鎿嶄綔绫诲瀷
+ *
+ * 鏋氫妇 {@link AfterSaleTicketLogActionEnum}
+ */
+ private Integer actionType;
+ /**
+ * 鎿嶄綔鍐呭/璇存槑
+ */
+ private String content;
+ /**
+ * 鎿嶄綔浜虹敤鎴风紪鍙�
+ */
+ private Long operatorUserId;
+ /**
+ * 缁忓害锛堢幇鍦虹鍒版椂涓婃姤锛�
+ */
+ private BigDecimal longitude;
+ /**
+ * 绾害锛堢幇鍦虹鍒版椂涓婃姤锛�
+ */
+ private BigDecimal latitude;
+ /**
+ * 瀹氫綅鍦板潃锛堢幇鍦虹鍒版椂涓婃姤锛�
+ */
+ private String address;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketLogMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketLogMapper.java
new file mode 100644
index 0000000..1bcb2e5
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketLogMapper.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.ticket;
+
+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.ticket.AfterSaleTicketLogDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟鎿嶄綔/杩涘害璁板綍 Mapper
+ */
+@Mapper
+public interface AfterSaleTicketLogMapper extends BaseMapperX<AfterSaleTicketLogDO> {
+
+ default List<AfterSaleTicketLogDO> selectListByTicketId(Long ticketId) {
+ return selectList(new LambdaQueryWrapperX<AfterSaleTicketLogDO>()
+ .eq(AfterSaleTicketLogDO::getTicketId, ticketId)
+ .orderByAsc(AfterSaleTicketLogDO::getId));
+ }
+
+ default List<AfterSaleTicketLogDO> selectListByTicketIds(Collection<Long> ticketIds) {
+ if (ticketIds == null || ticketIds.isEmpty()) {
+ return List.of();
+ }
+ return selectList(new LambdaQueryWrapperX<AfterSaleTicketLogDO>()
+ .in(AfterSaleTicketLogDO::getTicketId, ticketIds)
+ .orderByAsc(AfterSaleTicketLogDO::getId));
+ }
+
+ default Long selectCountByTicketIdAndActionType(Long ticketId, Integer actionType) {
+ return selectCount(new LambdaQueryWrapperX<AfterSaleTicketLogDO>()
+ .eq(AfterSaleTicketLogDO::getTicketId, ticketId)
+ .eq(AfterSaleTicketLogDO::getActionType, actionType));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketLogActionEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketLogActionEnum.java
new file mode 100644
index 0000000..f78d345
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketLogActionEnum.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗宸ュ崟鎿嶄綔/杩涘害璁板綍鍔ㄤ綔鏋氫妇
+ * <p>
+ * 鐢ㄤ簬銆屽伐鍗曞姪鎵嬨�岮pp 鐨勫伐鍗曞叏鐢熷懡鍛ㄦ湡杩芥函锛氭瘡涓�娆′笂鎶ャ�佹淳鍗曘�佺鍒般�佸鐞嗐�佽浆娲俱��
+ * 鍗囩骇銆佸畬缁撱�佸洖璁裤�佸綊妗c�佸彇娑堥兘浼氬啓鍏ヤ竴鏉� after_sale_ticket_log 璁板綍銆�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleTicketLogActionEnum implements ArrayValuable<Integer> {
+
+ CREATE(1, "宸ュ崟涓婃姤"),
+ ASSIGN(2, "娲惧崟"),
+ SIGN_IN(3, "鐜板満绛惧埌"),
+ CLASSIFY(4, "闂鍒ゅ畾"),
+ TRANSFER(5, "杞淳"),
+ ESCALATE(6, "鍗囩骇"),
+ HANDLE(7, "澶勭悊璁板綍"),
+ RESOLVE(8, "瀹岀粨宸ュ崟"),
+ VISIT(9, "瀹㈡埛鍥炶"),
+ CLOSE(10, "褰掓。"),
+ CANCEL(11, "鍙栨秷宸ュ崟");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleTicketLogActionEnum::getType).toArray(Integer[]::new);
+
+ private final Integer type;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static String getName(Integer type) {
+ return Arrays.stream(values())
+ .filter(item -> item.getType().equals(type))
+ .map(AfterSaleTicketLogActionEnum::getName)
+ .findFirst()
+ .orElse("鏈煡鎿嶄綔");
+ }
+
+}
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 d49adcf..a905135 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
@@ -20,6 +20,7 @@
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, "褰掓。澶辫触锛屽師鍥狅細宸ュ崟灏氭湭鍥炶閫氳繃锛岃鍏堢櫥璁版弧鎰忕殑鍥炶璁板綍");
+ ErrorCode TICKET_TRANSFER_FAIL_SAME_HANDLER = new ErrorCode(1_022_000_010, "杞淳澶辫触锛屽師鍥狅細鏂板鐞嗕汉涓庡綋鍓嶅鐞嗕汉鐩稿悓锛岃閫夋嫨鍏朵粬澶勭悊浜�");
// ========== 鍞悗宸ュ崟鏄庣粏琛� 1-022-001-000 ==========
ErrorCode TICKET_ITEM_NOT_EXISTS = new ErrorCode(1_022_001_000, "宸ュ崟鏄庣粏琛屼笉瀛樺湪");
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
index 7c3a9d0..f4e7840 100644
--- a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
@@ -1,10 +1,16 @@
package cn.iocoder.yudao.module.aftersales.service.ticket;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketEscalateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketHandleReqVO;
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.controller.admin.ticket.vo.ticket.AfterSaleTicketSignInReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketStatisticsRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketTransferReqVO;
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.dataobject.ticket.AfterSaleTicketLogDO;
import jakarta.validation.Valid;
import java.util.Collection;
@@ -74,10 +80,11 @@
/**
* 鍒嗛厤澶勭悊浜�
*
- * @param id 宸ュ崟缂栧彿
- * @param handlerUserId 澶勭悊浜虹敤鎴风紪鍙�
+ * @param id 宸ュ崟缂栧彿
+ * @param handlerUserId 澶勭悊浜虹敤鎴风紪鍙�
+ * @param operatorUserId 鎿嶄綔浜虹敤鎴风紪鍙�
*/
- void assignTicket(Long id, Long handlerUserId);
+ void assignTicket(Long id, Long handlerUserId, Long operatorUserId);
/**
* 闂鍒ゅ畾锛堢‘瀹氶棶棰樼被鍨嬨�佸垎绫汇�佷弗閲嶇▼搴︼級
@@ -86,28 +93,81 @@
* @param issueType 闂绫诲瀷
* @param issueCategory 闂鍒嗙被
* @param severityLevel 涓ラ噸绋嬪害
+ * @param operatorUserId 鎿嶄綔浜虹敤鎴风紪鍙�
*/
- void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel);
+ void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel, Long operatorUserId);
/**
* 瀹岀粨宸ュ崟
*
* @param id 宸ュ崟缂栧彿
+ * @param operatorUserId 鎿嶄綔浜虹敤鎴风紪鍙�
*/
- void resolveTicket(Long id);
+ void resolveTicket(Long id, Long operatorUserId);
/**
* 鍏抽棴宸ュ崟
*
* @param id 宸ュ崟缂栧彿
+ * @param operatorUserId 鎿嶄綔浜虹敤鎴风紪鍙�
*/
- void closeTicket(Long id);
+ void closeTicket(Long id, Long operatorUserId);
/**
* 鍙栨秷宸ュ崟
*
* @param id 宸ュ崟缂栧彿
+ * @param operatorUserId 鎿嶄綔浜虹敤鎴风紪鍙�
*/
- void cancelTicket(Long id);
+ void cancelTicket(Long id, Long operatorUserId);
+
+ /**
+ * 鐜板満绛惧埌锛堣褰曚綅缃俊鎭紝淇濋殰鏈嶅姟鐪熷疄鎬э級
+ *
+ * @param reqVO 绛惧埌淇℃伅
+ * @param userId 鎿嶄綔浜虹敤鎴风紪鍙�
+ */
+ void signInTicket(@Valid AfterSaleTicketSignInReqVO reqVO, Long userId);
+
+ /**
+ * 杞淳宸ュ崟
+ *
+ * @param reqVO 杞淳淇℃伅
+ * @param userId 鎿嶄綔浜虹敤鎴风紪鍙�
+ */
+ void transferTicket(@Valid AfterSaleTicketTransferReqVO reqVO, Long userId);
+
+ /**
+ * 鍗囩骇宸ュ崟
+ *
+ * @param reqVO 鍗囩骇淇℃伅
+ * @param userId 鎿嶄綔浜虹敤鎴风紪鍙�
+ */
+ void escalateTicket(@Valid AfterSaleTicketEscalateReqVO reqVO, Long userId);
+
+ /**
+ * 鏂板澶勭悊璁板綍
+ *
+ * @param reqVO 澶勭悊璁板綍
+ * @param userId 鎿嶄綔浜虹敤鎴风紪鍙�
+ */
+ void addHandleRecord(@Valid AfterSaleTicketHandleReqVO reqVO, Long userId);
+
+ /**
+ * 鑾峰緱宸ュ崟鎿嶄綔/杩涘害璁板綍鍒楄〃
+ *
+ * @param ticketId 宸ュ崟缂栧彿
+ * @return 璁板綍鍒楄〃
+ */
+ List<AfterSaleTicketLogDO> getTicketLogList(Long ticketId);
+
+ /**
+ * 鑾峰緱宸ュ崟鍔╂墜缁熻
+ *
+ * @param sceneType 鍦烘櫙绫诲瀷
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 缁熻缁撴灉
+ */
+ AfterSaleTicketStatisticsRespVO getTicketStatistics(Integer sceneType, Long userId);
}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
index bc01293..5a24cc7 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
@@ -1,17 +1,27 @@
package cn.iocoder.yudao.module.aftersales.service.ticket;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
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.ticket.vo.ticket.AfterSaleTicketEscalateReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketHandleReqVO;
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.controller.admin.ticket.vo.ticket.AfterSaleTicketSignInReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketStatisticsRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketTransferReqVO;
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.dataobject.ticket.AfterSaleTicketLogDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketLogMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleIssueTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketLogActionEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
@@ -19,15 +29,21 @@
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import cn.iocoder.yudao.module.aftersales.service.problem.AfterSaleProblemService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
@@ -44,9 +60,14 @@
private AfterSaleTicketMapper ticketMapper;
@Resource
private AfterSaleTicketItemMapper ticketItemMapper;
+ @Resource
+ private AfterSaleTicketLogMapper ticketLogMapper;
@Resource
private AfterSaleNoRedisDAO noRedisDAO;
+
+ @Resource
+ private AdminUserApi adminUserApi;
@Resource
private AfterSalePermissionService permissionService;
@@ -93,6 +114,10 @@
permBO.setUserId(userId);
permBO.setLevel(AfterSalePermissionLevelEnum.OWNER.getLevel());
permissionService.createPermission(permBO);
+
+ // 6. 璁板綍涓婃姤杩涘害
+ addLog(ticket.getId(), AfterSaleTicketLogActionEnum.CREATE,
+ StrUtil.blankToDefault(createReqVO.getIssueDescription(), "宸ュ崟涓婃姤锛�" + createReqVO.getName()), userId);
return ticket.getId();
}
@@ -162,25 +187,29 @@
@Override
@Transactional(rollbackFor = Exception.class)
- public void assignTicket(Long id, Long handlerUserId) {
+ public void assignTicket(Long id, Long handlerUserId, Long operatorUserId) {
AfterSaleTicketDO ticket = validateTicketExists(id);
validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
ticketMapper.updateById(new AfterSaleTicketDO().setId(id).setHandlerUserId(handlerUserId)
.setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus()));
+ addLog(id, AfterSaleTicketLogActionEnum.ASSIGN,
+ "娲惧崟缁欏鐞嗕汉锛�" + getUserName(handlerUserId), operatorUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
- public void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel) {
+ public void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel, Long operatorUserId) {
AfterSaleTicketDO ticket = validateTicketExists(id);
validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
.setIssueType(issueType).setIssueCategory(issueCategory).setSeverityLevel(severityLevel));
+ addLog(id, AfterSaleTicketLogActionEnum.CLASSIFY,
+ buildClassifyContent(issueType, issueCategory, severityLevel), operatorUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
- public void resolveTicket(Long id) {
+ public void resolveTicket(Long id, Long operatorUserId) {
AfterSaleTicketDO ticket = validateTicketExists(id);
validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING,
AfterSaleTicketStatusEnum.WAITING_RETURN, AfterSaleTicketStatusEnum.WAITING_REFUND);
@@ -189,11 +218,12 @@
.setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
.setResolvedTime(LocalDateTime.now())
.setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus()));
+ addLog(id, AfterSaleTicketLogActionEnum.RESOLVE, "宸ュ崟澶勭悊瀹屾垚锛岀瓑寰呭鎴峰洖璁�", operatorUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
- public void closeTicket(Long id) {
+ public void closeTicket(Long id, Long operatorUserId) {
// 1. 鏍¢獙宸ュ崟瀛樺湪涓斿浜庡凡瀹岀粨鐘舵��
AfterSaleTicketDO ticket = validateTicketExists(id);
validateTicketStatus(ticket, AfterSaleTicketStatusEnum.COMPLETED);
@@ -209,19 +239,241 @@
.setClosedTime(LocalDateTime.now())
.setProblemId(problem.getId())
.setProblemNo(problem.getNo()));
+ addLog(id, AfterSaleTicketLogActionEnum.CLOSE, "宸ュ崟褰掓。锛屾矇娣�闂锛�" + problem.getNo(), operatorUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
- public void cancelTicket(Long id) {
+ public void cancelTicket(Long id, Long operatorUserId) {
AfterSaleTicketDO ticket = validateTicketExists(id);
validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
.setStatus(AfterSaleTicketStatusEnum.CANCELED.getStatus()));
+ addLog(id, AfterSaleTicketLogActionEnum.CANCEL, "鍙栨秷宸ュ崟", operatorUserId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void signInTicket(AfterSaleTicketSignInReqVO reqVO, Long userId) {
+ AfterSaleTicketDO ticket = validateTicketExists(reqVO.getId());
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
+ // 1. 鍐欏叆绛惧埌杩涘害锛堝惈浣嶇疆淇℃伅锛�
+ LocalDateTime now = LocalDateTime.now();
+ AfterSaleTicketLogDO log = AfterSaleTicketLogDO.builder()
+ .ticketId(ticket.getId())
+ .actionType(AfterSaleTicketLogActionEnum.SIGN_IN.getType())
+ .content(StrUtil.blankToDefault(reqVO.getContent(), "宸插埌杈惧鎴风幇鍦虹鍒�"))
+ .operatorUserId(userId)
+ .longitude(reqVO.getLongitude())
+ .latitude(reqVO.getLatitude())
+ .address(StrUtil.maxLength(reqVO.getAddress(), 255))
+ .build();
+ ticketLogMapper.insert(log);
+ // 2. 棣栨绛惧埌鍥炲啓鍒板伐鍗曪紝渚夸簬鍒楄〃/璇︽儏蹇�熻瘑鍒槸鍚﹀凡鍒扮幇鍦�
+ if (ticket.getSignInTime() == null) {
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setSignInTime(now)
+ .setSignInAddress(StrUtil.maxLength(reqVO.getAddress(), 255)));
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void transferTicket(AfterSaleTicketTransferReqVO reqVO, Long userId) {
+ AfterSaleTicketDO ticket = validateTicketExists(reqVO.getId());
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
+ if (Objects.equals(ticket.getHandlerUserId(), reqVO.getHandlerUserId())) {
+ throw exception(TICKET_TRANSFER_FAIL_SAME_HANDLER);
+ }
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setHandlerUserId(reqVO.getHandlerUserId()));
+ String content = "杞淳缁欙細" + getUserName(reqVO.getHandlerUserId());
+ if (StrUtil.isNotBlank(reqVO.getReason())) {
+ content = content + "锛屽師鍥狅細" + reqVO.getReason();
+ }
+ addLog(ticket.getId(), AfterSaleTicketLogActionEnum.TRANSFER, content, userId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void escalateTicket(AfterSaleTicketEscalateReqVO reqVO, Long userId) {
+ AfterSaleTicketDO ticket = validateTicketExists(reqVO.getId());
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
+ int currentLevel = ticket.getSeverityLevel() == null ? 0 : ticket.getSeverityLevel();
+ int targetLevel = reqVO.getSeverityLevel() == null ? Math.min(currentLevel + 1, 3)
+ : Math.max(currentLevel, reqVO.getSeverityLevel());
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId()).setSeverityLevel(targetLevel));
+ addLog(ticket.getId(), AfterSaleTicketLogActionEnum.ESCALATE,
+ "涓ラ噸绋嬪害鍗囩骇涓猴細" + getSeverityName(targetLevel)
+ + (StrUtil.isNotBlank(reqVO.getReason()) ? "锛屽師鍥狅細" + reqVO.getReason() : ""),
+ userId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void addHandleRecord(AfterSaleTicketHandleReqVO reqVO, Long userId) {
+ AfterSaleTicketDO ticket = validateTicketExists(reqVO.getId());
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING,
+ AfterSaleTicketStatusEnum.WAITING_RETURN, AfterSaleTicketStatusEnum.WAITING_REFUND);
+ addLog(ticket.getId(), AfterSaleTicketLogActionEnum.HANDLE, reqVO.getContent(), userId);
+ }
+
+ @Override
+ public List<AfterSaleTicketLogDO> getTicketLogList(Long ticketId) {
+ validateTicketExists(ticketId);
+ return ticketLogMapper.selectListByTicketId(ticketId);
+ }
+
+ @Override
+ public AfterSaleTicketStatisticsRespVO getTicketStatistics(Integer sceneType, Long userId) {
+ // 1. 鎸夊綋鍓嶅満鏅殑鏁版嵁鏉冮檺鏌ヨ宸ュ崟锛堝伐鍗曞姪鎵嬮潰鍚戠Щ鍔ㄧ灏忔暟鎹噺鍦烘櫙锛�
+ AfterSaleTicketPageReqVO pageReqVO = new AfterSaleTicketPageReqVO();
+ pageReqVO.setPageNo(1);
+ pageReqVO.setPageSize(10000);
+ pageReqVO.setSceneType(sceneType == null ? 4 : sceneType);
+ List<AfterSaleTicketDO> tickets = getTicketPage(pageReqVO, userId).getList();
+
+ // 2. 鍩虹璁℃暟
+ AfterSaleTicketStatisticsRespVO respVO = new AfterSaleTicketStatisticsRespVO();
+ respVO.setTotalCount((long) tickets.size());
+ respVO.setProcessingCount(countStatus(tickets, AfterSaleTicketStatusEnum.PROCESSING));
+ respVO.setWaitingReturnCount(countStatus(tickets, AfterSaleTicketStatusEnum.WAITING_RETURN));
+ respVO.setWaitingRefundCount(countStatus(tickets, AfterSaleTicketStatusEnum.WAITING_REFUND));
+ respVO.setCompletedCount(countStatus(tickets, AfterSaleTicketStatusEnum.COMPLETED));
+ respVO.setClosedCount(countStatus(tickets, AfterSaleTicketStatusEnum.CLOSED));
+ respVO.setCanceledCount(countStatus(tickets, AfterSaleTicketStatusEnum.CANCELED));
+
+ LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+ respVO.setTodayCreateCount(tickets.stream()
+ .filter(t -> t.getCreateTime() != null && !t.getCreateTime().isBefore(todayStart)).count());
+ respVO.setTodayResolveCount(tickets.stream()
+ .filter(t -> t.getResolvedTime() != null && !t.getResolvedTime().isBefore(todayStart)).count());
+ respVO.setSignInCount(tickets.stream().filter(t -> t.getSignInTime() != null).count());
+
+ // 3. 鍗囩骇娆℃暟锛堟潵鑷繘搴﹁褰曪級
+ Set<Long> ticketIds = tickets.stream().map(AfterSaleTicketDO::getId).collect(Collectors.toSet());
+ List<AfterSaleTicketLogDO> logs = ticketLogMapper.selectListByTicketIds(ticketIds);
+ respVO.setEscalatedCount(logs.stream()
+ .filter(log -> AfterSaleTicketLogActionEnum.ESCALATE.getType().equals(log.getActionType()))
+ .map(AfterSaleTicketLogDO::getTicketId).distinct().count());
+
+ // 4. 骞冲潎瀹岀粨鑰楁椂
+ List<AfterSaleTicketDO> resolvedTickets = tickets.stream()
+ .filter(t -> t.getResolvedTime() != null && t.getCreateTime() != null)
+ .toList();
+ if (!resolvedTickets.isEmpty()) {
+ long totalMinutes = resolvedTickets.stream()
+ .mapToLong(t -> Math.max(Duration.between(t.getCreateTime(), t.getResolvedTime()).toMinutes(), 0))
+ .sum();
+ respVO.setAvgResolveHours(BigDecimal.valueOf(totalMinutes)
+ .divide(BigDecimal.valueOf(resolvedTickets.size() * 60L), 1, RoundingMode.HALF_UP));
+ } else {
+ respVO.setAvgResolveHours(BigDecimal.ZERO);
+ }
+
+ // 5. 鍥炶婊℃剰鐜囷紙浠呯粺璁″凡浜х敓鍥炶鐘舵�佺殑宸ュ崟锛�
+ List<AfterSaleTicketDO> visitedTickets = tickets.stream()
+ .filter(t -> t.getVisitStatus() != null).toList();
+ if (!visitedTickets.isEmpty()) {
+ long passed = visitedTickets.stream()
+ .filter(t -> AfterSaleVisitStatusEnum.PASSED.getStatus().equals(t.getVisitStatus())).count();
+ respVO.setSatisfactionRate(BigDecimal.valueOf(passed * 100L)
+ .divide(BigDecimal.valueOf(visitedTickets.size()), 1, RoundingMode.HALF_UP));
+ } else {
+ respVO.setSatisfactionRate(BigDecimal.ZERO);
+ }
+
+ // 6. 鎸夌姸鎬併�侀棶棰樼被鍨嬬粺璁�
+ respVO.setStatusCounts(Arrays.stream(AfterSaleTicketStatusEnum.values())
+ .map(status -> {
+ AfterSaleTicketStatisticsRespVO.StatusCount item = new AfterSaleTicketStatisticsRespVO.StatusCount();
+ item.setStatus(status.getStatus());
+ item.setStatusName(status.getName());
+ item.setCount(tickets.stream().filter(t -> status.getStatus().equals(t.getStatus())).count());
+ return item;
+ }).toList());
+ respVO.setIssueTypeCounts(Arrays.stream(AfterSaleIssueTypeEnum.values())
+ .map(issueType -> {
+ AfterSaleTicketStatisticsRespVO.IssueTypeCount item = new AfterSaleTicketStatisticsRespVO.IssueTypeCount();
+ item.setIssueType(issueType.getType());
+ item.setIssueTypeName(issueType.getName());
+ item.setCount(tickets.stream()
+ .filter(t -> Objects.equals(issueType.getType(),
+ t.getIssueType() == null ? AfterSaleIssueTypeEnum.UNCLASSIFIED.getType() : t.getIssueType()))
+ .count());
+ return item;
+ }).toList());
+
+ // 7. 鎸夊鐞嗕汉缁熻宸ヤ綔閲�
+ Map<Long, List<AfterSaleTicketDO>> handlerMap = tickets.stream()
+ .filter(t -> t.getHandlerUserId() != null)
+ .collect(Collectors.groupingBy(AfterSaleTicketDO::getHandlerUserId));
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(handlerMap.keySet());
+ respVO.setHandlerWorkloads(handlerMap.entrySet().stream().map(entry -> {
+ AfterSaleTicketStatisticsRespVO.HandlerWorkload item = new AfterSaleTicketStatisticsRespVO.HandlerWorkload();
+ item.setHandlerUserId(entry.getKey());
+ AdminUserRespDTO user = userMap.get(entry.getKey());
+ item.setHandlerUserName(user != null ? user.getNickname() : "鐢ㄦ埛" + entry.getKey());
+ item.setTotalCount((long) entry.getValue().size());
+ item.setProcessingCount(entry.getValue().stream()
+ .filter(t -> AfterSaleTicketStatusEnum.PROCESSING.getStatus().equals(t.getStatus())).count());
+ return item;
+ }).sorted(Comparator.comparing(AfterSaleTicketStatisticsRespVO.HandlerWorkload::getTotalCount).reversed()).toList());
+ return respVO;
}
// ========== 鍐呴儴鏂规硶 ==========
+ private Long countStatus(List<AfterSaleTicketDO> tickets, AfterSaleTicketStatusEnum status) {
+ return tickets.stream().filter(t -> status.getStatus().equals(t.getStatus())).count();
+ }
+
+ private void addLog(Long ticketId, AfterSaleTicketLogActionEnum action, String content, Long operatorUserId) {
+ ticketLogMapper.insert(AfterSaleTicketLogDO.builder()
+ .ticketId(ticketId)
+ .actionType(action.getType())
+ .content(content)
+ .operatorUserId(operatorUserId)
+ .build());
+ }
+
+ private String getUserName(Long userId) {
+ if (userId == null) {
+ return "鏈寚瀹�";
+ }
+ AdminUserRespDTO user = adminUserApi.getUserMap(Set.of(userId)).get(userId);
+ return user != null && StrUtil.isNotBlank(user.getNickname()) ? user.getNickname() : "鐢ㄦ埛" + userId;
+ }
+
+ private String buildClassifyContent(Integer issueType, String issueCategory, Integer severityLevel) {
+ StringBuilder content = new StringBuilder("闂鍒ゅ畾锛�").append(getIssueTypeName(issueType));
+ if (StrUtil.isNotBlank(issueCategory)) {
+ content.append("锛屽垎绫伙細").append(issueCategory);
+ }
+ if (severityLevel != null) {
+ content.append("锛屼弗閲嶇▼搴︼細").append(getSeverityName(severityLevel));
+ }
+ return content.toString();
+ }
+
+ private String getIssueTypeName(Integer issueType) {
+ return Arrays.stream(AfterSaleIssueTypeEnum.values())
+ .filter(item -> item.getType().equals(issueType))
+ .map(AfterSaleIssueTypeEnum::getName)
+ .findFirst()
+ .orElse("鏈垽瀹�");
+ }
+
+ private String getSeverityName(Integer severityLevel) {
+ return switch (severityLevel == null ? 0 : severityLevel) {
+ case 0 -> "杞诲井";
+ case 1 -> "涓�鑸�";
+ case 2 -> "涓ラ噸";
+ case 3 -> "鑷村懡";
+ default -> "鏈煡";
+ };
+ }
+
private AfterSaleTicketDO validateTicketExists(Long id) {
AfterSaleTicketDO ticket = ticketMapper.selectById(id);
if (ticket == null) {
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
index f30f75c..8ff9deb 100644
--- 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
@@ -6,9 +6,12 @@
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.ticket.AfterSaleTicketLogDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketLogMapper;
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.AfterSaleTicketLogActionEnum;
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;
@@ -38,6 +41,8 @@
private AfterSaleVisitMapper visitMapper;
@Resource
private AfterSaleTicketMapper ticketMapper;
+ @Resource
+ private AfterSaleTicketLogMapper ticketLogMapper;
@Resource
private StorageAttachmentApi storageAttachmentApi;
@@ -80,6 +85,16 @@
.setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus());
}
ticketMapper.updateById(update);
+
+ // 5. 璁板綍鍥炶杩涘害锛堝伐鍗曞姪鎵嬭繘搴﹁窡韪級
+ ticketLogMapper.insert(AfterSaleTicketLogDO.builder()
+ .ticketId(ticket.getId())
+ .actionType(AfterSaleTicketLogActionEnum.VISIT.getType())
+ .content("瀹㈡埛鍥炶锛�" + (satisfied ? "婊℃剰" : "涓嶆弧鎰�")
+ + (recurred ? "锛岄棶棰樺鍙�" : "锛屾湭澶嶅彂")
+ + (createReqVO.getContent() != null ? "锛�" + createReqVO.getContent() : ""))
+ .operatorUserId(userId)
+ .build());
return visitDO.getId();
}
--
Gitblit v1.9.3