feat(aftersales): 工单助手 App 闭环(签到/转派/升级/处理记录/进度/统计)
- after_sale_ticket 增加现场签到时间与地址
- 新增 after_sale_ticket_log 操作记录表,覆盖上报、派单、签到、判定、转派、升级、处理记录、完结、回访、归档、取消
- 新增签到、转派、升级、处理记录、进度列表、统计接口
- 回访登记同步写进度记录
- 补诚发五金本地库脚本:表结构 + 隐藏菜单/按钮权限 + 角色授权
Co-Authored-By: Claude Code <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # å·¥å婿ï¼Appï¼æ¥å£èè°æ¹æ¡ |
| | | |
| | | ## æ¶åé¡µé¢ |
| | | |
| | | - App å·¥ä½å°ãå·¥å婿ãåç» |
| | | - App å·¥å䏿¥ï¼æ°å¢ / ç¼è¾å®åå·¥åï¼ |
| | | - App å·¥ååçï¼å®åå·¥ååè¡¨ï¼ |
| | | - App å®åå·¥å详æ
ï¼è¿åº¦ãç¾å°ã转派ãå级ãå¤çè®°å½ï¼ |
| | | - App å·¥ååæï¼ç»è®¡çæ¿ï¼ |
| | | |
| | | ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å
¥ |
| | | |
| | | 1. å·¥å䏿¥ â å·¥ååçï¼ä¸æ¥åå·¥åç¶æä¸ºãå¤çä¸ãï¼èªå¨åå
¥ä¸æ¡ `CREATE` è¿åº¦è®°å½ã |
| | | 2. å·¥ååç â å®åå·¥å详æ
ï¼å表ç¹å
¥è¯¦æ
ï¼æ `ticketId` æåè¿åº¦è®°å½ãå访记å½ä¸éä»¶ã |
| | | 3. 详æ
â ç°åºç¾å°ï¼App åå½åå®ä½ï¼ç»åº¦ / 纬度ï¼å¹¶å¡«åç°åºå°åï¼é¦æ¬¡ç¾å°ååå·¥å `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 | å¦ | ç»åº¦ï¼App å®ä½å¤±è´¥æ¶å
许为空 | |
| | | | 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 æè´è´£ç / 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ï¼token å¤±ææ¶éç¨æ·éæ°ç»å½ååè¡¥ä¼ ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ===================================================================== |
| | | -- è¯åäºé · å·¥å婿ï¼Appï¼å端é
å¥èæ¬ |
| | | -- 便®ãéä»¶ä¸ï¼ææ¯æ¹æ¡-è¯åäºéï¼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. å·¥å婿èåï¼App 使ç¨ï¼Web 侧䏿¾ç¤ºï¼ ==================== |
| | | 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 è§è²ææï¼éå®ãç产管çåãç»çï¼è¶
级管çåç±ä»£ç ç´éï¼æ éè¡ï¼ |
| | | 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`; |
| | |
| | | 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; |
| | |
| | | @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); |
| | | } |
| | | |
| | |
| | | @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); |
| | | } |
| | | |
| | |
| | | @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); |
| | | } |
| | | |
| | |
| | | @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); |
| | | } |
| | | |
| | |
| | | @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-æè´è´£ç 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) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 = "æ²³åçå山叿»¦åå¿XXè·¯XXå·") |
| | | private String address; |
| | | |
| | | @Schema(description = "æä½æ¶é´") |
| | | private LocalDateTime createTime; |
| | | |
| | | } |
| | |
| | | @Schema(description = "å
³è维修记å½ç¼å·", example = "1") |
| | | private Long repairId; |
| | | |
| | | @Schema(description = "馿¬¡ç°åºç¾å°æ¶é´") |
| | | private LocalDateTime signInTime; |
| | | |
| | | @Schema(description = "馿¬¡ç°åºç¾å°å°å") |
| | | private String signInAddress; |
| | | |
| | | @Schema(description = "è§£å³æ¶é´") |
| | | private LocalDateTime resolvedTime; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 = "æ²³åçå山叿»¦åå¿XXè·¯XXå·") |
| | | private String address; |
| | | |
| | | @Schema(description = "ç¾å°è¯´æ", example = "å·²å°è¾¾å®¢æ·ç°åº") |
| | | private String content; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| | |
| | | */ |
| | | private Long repairId; |
| | | /** |
| | | * 馿¬¡ç°åºç¾å°æ¶é´ |
| | | */ |
| | | private LocalDateTime signInTime; |
| | | /** |
| | | * 馿¬¡ç°åºç¾å°å°å |
| | | */ |
| | | private String signInAddress; |
| | | /** |
| | | * è§£å³æ¶é´ |
| | | */ |
| | | private LocalDateTime resolvedTime; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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)); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * ç¨äºãå·¥å婿ãApp çå·¥åå
¨çå½å¨æè¿½æº¯ï¼æ¯ä¸æ¬¡ä¸æ¥ãæ´¾åãç¾å°ãå¤çã转派ã |
| | | * å级ãå®ç»ãå访ã彿¡£ãåæ¶é½ä¼åå
¥ä¸æ¡ 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("æªç¥æä½"); |
| | | } |
| | | |
| | | } |
| | |
| | | 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, "å·¥åæç»è¡ä¸åå¨"); |
| | |
| | | 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; |
| | |
| | | /** |
| | | * åé
å¤ç人 |
| | | * |
| | | * @param id å·¥åç¼å· |
| | | * @param handlerUserId å¤çäººç¨æ·ç¼å· |
| | | * @param id å·¥åç¼å· |
| | | * @param handlerUserId å¤çäººç¨æ·ç¼å· |
| | | * @param operatorUserId æä½äººç¨æ·ç¼å· |
| | | */ |
| | | void assignTicket(Long id, Long handlerUserId); |
| | | void assignTicket(Long id, Long handlerUserId, Long operatorUserId); |
| | | |
| | | /** |
| | | * é®é¢å¤å®ï¼ç¡®å®é®é¢ç±»åãåç±»ã严éç¨åº¦ï¼ |
| | |
| | | * @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); |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | 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.*; |
| | |
| | | private AfterSaleTicketMapper ticketMapper; |
| | | @Resource |
| | | private AfterSaleTicketItemMapper ticketItemMapper; |
| | | @Resource |
| | | private AfterSaleTicketLogMapper ticketLogMapper; |
| | | |
| | | @Resource |
| | | private AfterSaleNoRedisDAO noRedisDAO; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | |
| | | @Resource |
| | | private AfterSalePermissionService permissionService; |
| | |
| | | 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(); |
| | | } |
| | |
| | | |
| | | @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); |
| | |
| | | .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); |
| | |
| | | .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) { |
| | |
| | | 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; |
| | |
| | | private AfterSaleVisitMapper visitMapper; |
| | | @Resource |
| | | private AfterSaleTicketMapper ticketMapper; |
| | | @Resource |
| | | private AfterSaleTicketLogMapper ticketLogMapper; |
| | | @Resource |
| | | private StorageAttachmentApi storageAttachmentApi; |
| | | |
| | |
| | | .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(); |
| | | } |
| | | |