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