diff --git a/modules/core/src/main/java/com/bytedesk/core/constant/I18Consts.java b/modules/core/src/main/java/com/bytedesk/core/constant/I18Consts.java index 7a7867cb5a..f32111c859 100644 --- a/modules/core/src/main/java/com/bytedesk/core/constant/I18Consts.java +++ b/modules/core/src/main/java/com/bytedesk/core/constant/I18Consts.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-04-26 22:25:47 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-19 14:45:04 + * @LastEditTime: 2024-12-19 15:13:16 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -27,6 +27,7 @@ public class I18Consts { public static final String I18N_EMAIL_ALREADY_EXISTS = I18N_PREFIX + "email.already.exists"; public static final String I18N_MOBILE_NOT_EXISTS = I18N_PREFIX + "mobile.not.exists"; public static final String I18N_EMAIL_NOT_EXISTS = I18N_PREFIX + "email.not.exists"; + public static final String I18N_MOBILE_FORMAT_ERROR = I18N_PREFIX + "mobile.format.error"; public static final String I18N_EMAIL_FORMAT_ERROR = I18N_PREFIX + "email.format.error"; // captcha public static final String I18N_AUTH_CAPTCHA_SEND_SUCCESS = I18N_PREFIX + "auth.captcha.send.success"; diff --git a/modules/core/src/main/java/com/bytedesk/core/enums/ClientEnum.java b/modules/core/src/main/java/com/bytedesk/core/enums/ClientEnum.java index 2f8c2b5681..02aa3b689e 100644 --- a/modules/core/src/main/java/com/bytedesk/core/enums/ClientEnum.java +++ b/modules/core/src/main/java/com/bytedesk/core/enums/ClientEnum.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-05-25 13:07:20 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-11-20 14:24:50 + * @LastEditTime: 2024-12-19 15:52:30 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -51,6 +51,64 @@ public enum ClientEnum { WECHAT_WORK, WECHAT_KEFU, WECHAT_CHANNEL, + // + // 社交媒体渠道 + XIAOHONGSHU, // 小红书 + DOUYIN, // 抖音 + KUAISHOU, // 快手 + BILIBILI, // B站 + WEIBO, // 微博 + ZHIHU, // 知乎 + TOUTIAO, // 头条 + DOUBAN, // 豆瓣 + // + // 电商渠道 + TAOBAO, // 淘宝 + TMALL, // 天猫 + JD, // 京东 + PINDUODUO, // 拼多多 + MEITUAN, // 美团 + ELEME, // 饿了么 + DIANPING, // 大众点评 + // + // 企业渠道 + DINGTALK, // 钉钉 + FEISHU, // 飞书 + WECOM, // 企业微信 + // + // 其他渠道 + EMAIL, // 邮件 + SMS, // 短信 + PHONE, // 电话 + // + // 海外社交媒体 + TWITTER, // Twitter/X + FACEBOOK, // Facebook + INSTAGRAM, // Instagram + LINKEDIN, // LinkedIn + YOUTUBE, // YouTube + TIKTOK, // TikTok + PINTEREST, // Pinterest + REDDIT, // Reddit + SNAPCHAT, // Snapchat + // + // 海外即时通讯 + WHATSAPP, // WhatsApp + TELEGRAM, // Telegram + LINE, // LINE + KAKAO, // KakaoTalk + VIBER, // Viber + SIGNAL, // Signal + DISCORD, // Discord + SLACK, // Slack + MESSENGER, // Facebook Messenger + // + // 海外电商 + AMAZON, // 亚马逊 + EBAY, // eBay + SHOPIFY, // Shopify + LAZADA, // 来赞达 + SHOPEE, // 虾皮 ; // 根据字符串查找对应的枚举常量 diff --git a/modules/core/src/main/java/com/bytedesk/core/thread/ThreadStateEnum.java b/modules/core/src/main/java/com/bytedesk/core/thread/ThreadStateEnum.java index 390a53fba6..d95403ed9b 100644 --- a/modules/core/src/main/java/com/bytedesk/core/thread/ThreadStateEnum.java +++ b/modules/core/src/main/java/com/bytedesk/core/thread/ThreadStateEnum.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-05-25 10:43:58 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-10-17 17:36:50 + * @LastEditTime: 2024-12-19 15:55:33 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -20,6 +20,10 @@ public enum ThreadStateEnum { STARTED, // 开始会话 OFFLINE, // 客服不在线 CLOSED, // 会话已结束 + // TRANSFERRED, // 转接 + // REJECTED, // 拒绝会话 + // CANCELLED, // 取消会话 + // RESOLVED, // 解决问题 ; // 根据字符串查找对应的枚举常量 diff --git a/modules/service/src/main/java/com/bytedesk/service/agent/AgentEntity.java b/modules/service/src/main/java/com/bytedesk/service/agent/AgentEntity.java index 35fc1e14f0..a88d852b5f 100644 --- a/modules/service/src/main/java/com/bytedesk/service/agent/AgentEntity.java +++ b/modules/service/src/main/java/com/bytedesk/service/agent/AgentEntity.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-01-29 16:19:51 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-19 14:20:53 + * @LastEditTime: 2024-12-19 15:13:35 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -73,9 +73,8 @@ public class AgentEntity extends BaseEntity { @Builder.Default private String description = I18Consts.I18N_USER_DESCRIPTION; - // only support chinese mobile number, - // TODO: support other country mobile number using libphonenumber library - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "Invalid mobile number format") + // only support chinese mobile number, TODO: support other country mobile number using libphonenumber library + @Pattern(regexp = "^1[3-9]\\d{9}$", message = I18Consts.I18N_MOBILE_FORMAT_ERROR) private String mobile; @Email(message = I18Consts.I18N_EMAIL_FORMAT_ERROR) diff --git a/modules/service/src/main/java/com/bytedesk/service/agent/AgentServiceImpl.java b/modules/service/src/main/java/com/bytedesk/service/agent/AgentServiceImpl.java index 45b1d32619..311cc083b4 100644 --- a/modules/service/src/main/java/com/bytedesk/service/agent/AgentServiceImpl.java +++ b/modules/service/src/main/java/com/bytedesk/service/agent/AgentServiceImpl.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-12-07 11:30:15 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-09 07:16:37 + * @LastEditTime: 2024-12-19 15:56:56 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. diff --git a/modules/service/src/main/java/com/bytedesk/service/queue_member/QueueMemberEntity.java b/modules/service/src/main/java/com/bytedesk/service/queue_member/QueueMemberEntity.java index fa0e0ea03f..e91b5aa711 100644 --- a/modules/service/src/main/java/com/bytedesk/service/queue_member/QueueMemberEntity.java +++ b/modules/service/src/main/java/com/bytedesk/service/queue_member/QueueMemberEntity.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-10-14 17:23:58 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-19 14:58:45 + * @LastEditTime: 2024-12-19 15:21:29 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -74,8 +74,8 @@ public class QueueMemberEntity extends BaseEntity { @Column(name = "first_response") private boolean firstResponse; // 是否首次响应 - @Column(name = "end_time") - private LocalDateTime endTime; // 结束时间 + @Column(name = "close_time") + private LocalDateTime closeTime; // 结束时间 @Column(name = "agent_uid") private String agentUid; // 服务客服 @@ -103,7 +103,7 @@ public class QueueMemberEntity extends BaseEntity { if (QueueMemberStatusEnum.PROCESSING.name().equals(newStatus)) { this.startTime = LocalDateTime.now(); } else if (QueueMemberStatusEnum.valueOf(newStatus).isEndStatus()) { - this.endTime = LocalDateTime.now(); + this.closeTime = LocalDateTime.now(); } } } diff --git a/modules/service/src/main/java/com/bytedesk/service/route/IRouteService.java b/modules/service/src/main/java/com/bytedesk/service/routing/IRouteService.java similarity index 97% rename from modules/service/src/main/java/com/bytedesk/service/route/IRouteService.java rename to modules/service/src/main/java/com/bytedesk/service/routing/IRouteService.java index df83b347de..d2f79a2a15 100644 --- a/modules/service/src/main/java/com/bytedesk/service/route/IRouteService.java +++ b/modules/service/src/main/java/com/bytedesk/service/routing/IRouteService.java @@ -12,7 +12,7 @@ * 联系:270580156@qq.com * Copyright (c) 2024 by bytedesk.com, All Rights Reserved. */ -package com.bytedesk.service.route; +package com.bytedesk.service.routing; // import com.bytedesk.ai.robot.RobotEntity; // import com.bytedesk.core.message.MessageProtobuf; diff --git a/modules/service/src/main/java/com/bytedesk/service/route/RouteService.java b/modules/service/src/main/java/com/bytedesk/service/routing/RouteService.java similarity index 99% rename from modules/service/src/main/java/com/bytedesk/service/route/RouteService.java rename to modules/service/src/main/java/com/bytedesk/service/routing/RouteService.java index b92c3b74d4..4198ed92ac 100644 --- a/modules/service/src/main/java/com/bytedesk/service/route/RouteService.java +++ b/modules/service/src/main/java/com/bytedesk/service/routing/RouteService.java @@ -12,7 +12,7 @@ * 联系:270580156@qq.com * Copyright (c) 2024 by bytedesk.com, All Rights Reserved. */ -package com.bytedesk.service.route; +package com.bytedesk.service.routing; // import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; diff --git a/modules/service/src/main/java/com/bytedesk/service/route/RouteServiceImpl.java b/modules/service/src/main/java/com/bytedesk/service/routing/RouteServiceImpl.java similarity index 99% rename from modules/service/src/main/java/com/bytedesk/service/route/RouteServiceImpl.java rename to modules/service/src/main/java/com/bytedesk/service/routing/RouteServiceImpl.java index 59d6c1afd5..e9d5d67a12 100644 --- a/modules/service/src/main/java/com/bytedesk/service/route/RouteServiceImpl.java +++ b/modules/service/src/main/java/com/bytedesk/service/routing/RouteServiceImpl.java @@ -12,7 +12,7 @@ * 联系:270580156@qq.com * Copyright (c) 2024 by bytedesk.com, All Rights Reserved. */ -package com.bytedesk.service.route; +package com.bytedesk.service.routing; // import org.springframework.stereotype.Service; diff --git a/modules/service/src/main/java/com/bytedesk/service/strategy/AgentCsThreadCreationStrategy.java b/modules/service/src/main/java/com/bytedesk/service/strategy/AgentCsThreadCreationStrategy.java index 7f7a21b646..94f1d73a91 100644 --- a/modules/service/src/main/java/com/bytedesk/service/strategy/AgentCsThreadCreationStrategy.java +++ b/modules/service/src/main/java/com/bytedesk/service/strategy/AgentCsThreadCreationStrategy.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-07-15 15:58:11 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-19 12:36:13 + * @LastEditTime: 2024-12-19 15:47:27 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -28,10 +28,7 @@ import com.bytedesk.core.thread.ThreadRestService; import com.bytedesk.core.topic.TopicUtils; import com.bytedesk.service.agent.AgentEntity; import com.bytedesk.service.agent.AgentRestService; -// import com.bytedesk.service.queue.QueueResponse; -// import com.bytedesk.service.queue.QueueServiceMy; -// import com.bytedesk.service.counter_visitor.CounterVisitorService; -import com.bytedesk.service.route.RouteService; +import com.bytedesk.service.routing.RouteService; import com.bytedesk.service.visitor.VisitorRequest; import com.bytedesk.service.visitor_thread.VisitorThreadService; @@ -95,11 +92,8 @@ public class AgentCsThreadCreationStrategy implements CsThreadCreationStrategy { // 计数器,排队号 // String orgUid = visitorRequest.getOrgUid(); // String visitor = ConvertServiceUtils.convertToUserProtobufJSONString(visitorRequest); - // QueueMemberResponse counter = counterService.getNumber(orgUid, topic, visitor); - // counterVisitorService.saveNumber(orgUid, topic, counter.getCurrentNumber(), visitor); - // log.info("counter topic: {}, count {}", topic, counter.getCurrentNumber()); // - // thread = visitorThreadService.reInitAgentThreadExtra(thread, agent); + thread = visitorThreadService.reInitAgentThreadExtra(thread, agent); // QueueResponse queueResponse = visitorQueueService.enqueue(thread); // log.info("Enqueued to queue {}", queueResponse.toString()); // thread.setSerialNumber(counter.getCurrentNumber()); diff --git a/modules/service/src/main/java/com/bytedesk/service/strategy/WorkgroupCsThreadCreationStrategy.java b/modules/service/src/main/java/com/bytedesk/service/strategy/WorkgroupCsThreadCreationStrategy.java index 03e5589f0d..8612e9a2c8 100644 --- a/modules/service/src/main/java/com/bytedesk/service/strategy/WorkgroupCsThreadCreationStrategy.java +++ b/modules/service/src/main/java/com/bytedesk/service/strategy/WorkgroupCsThreadCreationStrategy.java @@ -25,7 +25,7 @@ import com.bytedesk.core.message.MessageProtobuf; import com.bytedesk.core.rbac.user.UserProtobuf; import com.bytedesk.core.thread.ThreadRestService; import com.bytedesk.core.topic.TopicUtils; -import com.bytedesk.service.route.RouteService; +import com.bytedesk.service.routing.RouteService; import com.bytedesk.service.visitor.VisitorRequest; import com.bytedesk.service.visitor_thread.VisitorThreadService; import com.bytedesk.service.workgroup.WorkgroupEntity; diff --git a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAgentRepository.java b/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAgentRepository.java index 8c18954e46..3cb1e58e64 100644 --- a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAgentRepository.java +++ b/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAgentRepository.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-12-07 11:37:41 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-09 07:16:28 + * @LastEditTime: 2024-12-19 15:56:39 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. diff --git a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentService.java b/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentService.java index 75271e95d1..bd3d78478b 100644 --- a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentService.java +++ b/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentService.java @@ -1,61 +1,239 @@ -/* - * @Author: jackning 270580156@qq.com - * @Date: 2024-12-07 11:17:07 - * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-07 20:17:07 - * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk - * Please be aware of the BSL license restrictions before installing Bytedesk IM – - * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. - * 仅支持企业内部员工自用,严禁私自用于销售、二次销售或者部署SaaS方式销售 - * Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE - * contact: 270580156@qq.com - * 联系:270580156@qq.com - * Copyright (c) 2024 by bytedesk.com, All Rights Reserved. - */ package com.bytedesk.service.thread; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.bytedesk.core.thread.ThreadEntity; +import com.bytedesk.core.thread.ThreadRepository; import com.bytedesk.service.agent.AgentEntity; +import com.bytedesk.service.agent.AgentRepository; +import com.bytedesk.service.agent.AgentService; +import com.bytedesk.service.queue.QueueService; -/** - * 会话分配服务 - * 负责将会话分配给合适的客服 - */ -public interface ThreadAssignmentService { +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class ThreadAssignmentService { + + @Autowired + private ThreadRepository threadRepository; + + @Autowired + private AgentRepository agentRepository; + + @Autowired + private AgentService agentService; + + @Autowired + private QueueService queueService; /** * 处理新会话分配 - * @param thread 会话 */ - public void handleNewThread(ThreadEntity thread); + @Transactional + public void handleNewThread(ThreadEntity thread) { + try { + // 1. 获取可用客服 + List availableAgents = agentService.getAvailableAgents(); + + if(availableAgents.isEmpty()) { + // 无可用客服,加入排队队列 + int priority = calculatePriority(thread); + queueService.enqueue(thread.getUid(), priority); + // thread.setStatus("queued"); + threadRepository.save(thread); + log.info("No available agents, thread {} added to queue", thread.getUid()); + return; + } + + // 2. 根据分配策略选择客服 + AgentEntity selectedAgent = selectAgent(availableAgents, thread); + + // 3. 分配会话 + assignToAgent(thread, selectedAgent); + + } catch(Exception e) { + log.error("Failed to handle new thread", e); + // 异常情况加入队列 + queueService.enqueue(thread.getUid(), 0); + // thread.setStatus("queued"); + threadRepository.save(thread); + } + } /** - * 从可用客服中选择最合适的客服 - * @param availableAgents 可用客服列表 - * @param thread 待分配的会话 - * @return 选中的客服 + * 计算会话优先级 */ - AgentEntity selectAgent(List availableAgents, ThreadEntity thread); + private int calculatePriority(ThreadEntity thread) { + int priority = 0; + + // 1. VIP访客优先级 + // if(thread.getVisitor().isVip()) { + // priority += 100; + // } + + // 2. 老客户优先级 + // if(thread.getVisitor().getVisitCount() > 10) { + // priority += 50; + // } + + // 3. 紧急程度 + // if(thread.isUrgent()) { + // priority += 30; + // } + + return priority; + } - /** - * 将会话分配给指定客服 - * @param thread 会话 - * @param agent 客服 - */ - void assignToAgent(ThreadEntity thread, AgentEntity agent); + // 从可用客服中选择最合适的客服 + public AgentEntity selectAgent(List availableAgents, ThreadEntity thread) { + // 获取会话所需技能 + List requiredSkills = thread.getRequiredSkillList(); + + // 过滤出具备所需技能的客服 + List qualifiedAgents = availableAgents.stream() + .filter(agent -> agent.hasRequiredSkills(requiredSkills)) + .collect(Collectors.toList()); + + if (qualifiedAgents.isEmpty()) { + // 如果没有完全匹配的客服,可以选择部分匹配的 + qualifiedAgents = findPartiallyMatchedAgents(availableAgents, requiredSkills); + } + + // 在具备技能的客服中选择工作负载最小的 + return qualifiedAgents.stream() + .filter(AgentEntity::canAcceptMore) + .min(Comparator.comparingInt(AgentEntity::getCurrentThreadCount)) + .orElseThrow(() -> new RuntimeException("No qualified agent available")); + } + + // 查找部分技能匹配的客服 + private List findPartiallyMatchedAgents( + List agents, List requiredSkills) { + + // 计算每个客服的技能匹配度 + Map matchScores = new HashMap<>(); + for (AgentEntity agent : agents) { + int score = calculateSkillMatchScore(agent, requiredSkills); + matchScores.put(agent, score); + } + + // 选择匹配度最高的客服 + int maxScore = matchScores.values().stream() + .mapToInt(Integer::intValue) + .max() + .orElse(0); + + return agents.stream() + .filter(agent -> matchScores.get(agent) == maxScore) + .collect(Collectors.toList()); + } + + // 计算技能匹配分数 + private int calculateSkillMatchScore(AgentEntity agent, List requiredSkills) { + return (int) requiredSkills.stream() + .filter(agent::hasSkill) + .count(); + } - /** - * 取消会话分配 - * @param thread 会话 - */ - void unassignThread(ThreadEntity thread); + // 将会话分配给指定客服 + public AgentEntity assignThread(ThreadEntity thread) { + // 1. 先按工作组筛选 + // WorkgroupEntity workgroup = thread.getWorkgroup(); + // List workgroupAgents = workgroup.getAvailableAgents(); + + // // 2. 再按技能筛选 + // List requiredSkills = thread.getRequiredSkills(); + // List qualifiedAgents = workgroupAgents.stream() + // .filter(a -> a.hasRequiredSkills(requiredSkills)) + // .collect(Collectors.toList()); + + // // 3. 最后按工作组路由规则选择 + // return workgroup.getRoutingStrategy().select(qualifiedAgents); - /** - * 重新分配会话 - * @param thread 会话 - * @return 是否成功重新分配 - */ - boolean reassignThread(ThreadEntity thread); -} + return null; + } + + + @Transactional + public void assignToAgent(ThreadEntity thread, AgentEntity agent) { + // 如果在队列中,先移出队列 + // if("queued".equals(thread.getStatus())) { + // queueService.dequeue(thread.getUid(), QueueStatusEnum.COMPLETED); + // } + + // 更新会话信息 + // thread.setAgentUid(agent.getUid()); + // thread.setAssignTime(LocalDateTime.now()); + // thread.setStatus("assigned"); + threadRepository.save(thread); + + // 更新客服工作负载 + agent.increaseThreadCount(); + agentRepository.save(agent); + + log.info("Thread {} assigned to agent {}", thread.getUid(), agent.getUid()); + } + + // 取消会话分配 + + @Transactional + public void unassignThread(ThreadEntity thread) { + String currentAgentUid = "";// thread.getAgentUid(); + if (currentAgentUid != null) { + // 更新客服工作负载 + AgentEntity agent = agentRepository.findByUid(currentAgentUid) + .orElseThrow(() -> new RuntimeException("Agent not found")); + agent.decreaseThreadCount(); + agentRepository.save(agent); + } + + // 更新会话信息 + // thread.setAgentUid(null); + // thread.setStatus("unassigned"); + threadRepository.save(thread); + + log.info("Thread {} unassigned from agent {}", thread.getUid(), currentAgentUid); + } + + // 重新分配会话 + + @Transactional + public boolean reassignThread(ThreadEntity thread) { + try { + // 获取可用客服 + List availableAgents = agentService.getAvailableAgents(); + if (availableAgents.isEmpty()) { + // 无可用客服,加入排队队列 + queueService.enqueue(thread.getUid(), calculatePriority(thread)); + // thread.setStatus("queued"); + threadRepository.save(thread); + return false; + } + + // 取消当前分配 + unassignThread(thread); + + // 选择新客服并分配 + AgentEntity newAgent = selectAgent(availableAgents, thread); + assignToAgent(thread, newAgent); + + return true; + } catch (Exception e) { + log.error("Failed to reassign thread {}", thread.getUid(), e); + // 异常情况加入队列 + queueService.enqueue(thread.getUid(), 0); + // thread.setStatus("queued"); + threadRepository.save(thread); + return false; + } + } +} \ No newline at end of file diff --git a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentServiceImpl.java b/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentServiceImpl.java deleted file mode 100644 index 5064439c20..0000000000 --- a/modules/service/src/main/java/com/bytedesk/service/thread/ThreadAssignmentServiceImpl.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.bytedesk.service.thread; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.bytedesk.core.thread.ThreadEntity; -import com.bytedesk.core.thread.ThreadRepository; -import com.bytedesk.service.agent.AgentEntity; -import com.bytedesk.service.agent.AgentRepository; -import com.bytedesk.service.agent.AgentService; -import com.bytedesk.service.queue.QueueService; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -public class ThreadAssignmentServiceImpl implements ThreadAssignmentService { - - @Autowired - private ThreadRepository threadRepository; - - @Autowired - private AgentRepository agentRepository; - - @Autowired - private AgentService agentService; - - @Autowired - private QueueService queueService; - - /** - * 处理新会话分配 - */ - @Transactional - public void handleNewThread(ThreadEntity thread) { - try { - // 1. 获取可用客服 - List availableAgents = agentService.getAvailableAgents(); - - if(availableAgents.isEmpty()) { - // 无可用客服,加入排队队列 - int priority = calculatePriority(thread); - queueService.enqueue(thread.getUid(), priority); - // thread.setStatus("queued"); - threadRepository.save(thread); - log.info("No available agents, thread {} added to queue", thread.getUid()); - return; - } - - // 2. 根据分配策略选择客服 - AgentEntity selectedAgent = selectAgent(availableAgents, thread); - - // 3. 分配会话 - assignToAgent(thread, selectedAgent); - - } catch(Exception e) { - log.error("Failed to handle new thread", e); - // 异常情况加入队列 - queueService.enqueue(thread.getUid(), 0); - // thread.setStatus("queued"); - threadRepository.save(thread); - } - } - - /** - * 计算会话优先级 - */ - private int calculatePriority(ThreadEntity thread) { - int priority = 0; - - // 1. VIP访客优先级 - // if(thread.getVisitor().isVip()) { - // priority += 100; - // } - - // 2. 老客户优先级 - // if(thread.getVisitor().getVisitCount() > 10) { - // priority += 50; - // } - - // 3. 紧急程度 - // if(thread.isUrgent()) { - // priority += 30; - // } - - return priority; - } - - @Override - public AgentEntity selectAgent(List availableAgents, ThreadEntity thread) { - // 获取会话所需技能 - List requiredSkills = thread.getRequiredSkillList(); - - // 过滤出具备所需技能的客服 - List qualifiedAgents = availableAgents.stream() - .filter(agent -> agent.hasRequiredSkills(requiredSkills)) - .collect(Collectors.toList()); - - if (qualifiedAgents.isEmpty()) { - // 如果没有完全匹配的客服,可以选择部分匹配的 - qualifiedAgents = findPartiallyMatchedAgents(availableAgents, requiredSkills); - } - - // 在具备技能的客服中选择工作负载最小的 - return qualifiedAgents.stream() - .filter(AgentEntity::canAcceptMore) - .min(Comparator.comparingInt(AgentEntity::getCurrentThreadCount)) - .orElseThrow(() -> new RuntimeException("No qualified agent available")); - } - - // 查找部分技能匹配的客服 - private List findPartiallyMatchedAgents( - List agents, List requiredSkills) { - - // 计算每个客服的技能匹配度 - Map matchScores = new HashMap<>(); - for (AgentEntity agent : agents) { - int score = calculateSkillMatchScore(agent, requiredSkills); - matchScores.put(agent, score); - } - - // 选择匹配度最高的客服 - int maxScore = matchScores.values().stream() - .mapToInt(Integer::intValue) - .max() - .orElse(0); - - return agents.stream() - .filter(agent -> matchScores.get(agent) == maxScore) - .collect(Collectors.toList()); - } - - // 计算技能匹配分数 - private int calculateSkillMatchScore(AgentEntity agent, List requiredSkills) { - return (int) requiredSkills.stream() - .filter(agent::hasSkill) - .count(); - } - - public AgentEntity assignThread(ThreadEntity thread) { - // 1. 先按工作组筛选 - // WorkgroupEntity workgroup = thread.getWorkgroup(); - // List workgroupAgents = workgroup.getAvailableAgents(); - - // // 2. 再按技能筛选 - // List requiredSkills = thread.getRequiredSkills(); - // List qualifiedAgents = workgroupAgents.stream() - // .filter(a -> a.hasRequiredSkills(requiredSkills)) - // .collect(Collectors.toList()); - - // // 3. 最后按工作组路由规则选择 - // return workgroup.getRoutingStrategy().select(qualifiedAgents); - - return null; - } - - @Override - @Transactional - public void assignToAgent(ThreadEntity thread, AgentEntity agent) { - // 如果在队列中,先移出队列 - // if("queued".equals(thread.getStatus())) { - // queueService.dequeue(thread.getUid(), QueueStatusEnum.COMPLETED); - // } - - // 更新会话信息 - // thread.setAgentUid(agent.getUid()); - // thread.setAssignTime(LocalDateTime.now()); - // thread.setStatus("assigned"); - threadRepository.save(thread); - - // 更新客服工作负载 - agent.increaseThreadCount(); - agentRepository.save(agent); - - log.info("Thread {} assigned to agent {}", thread.getUid(), agent.getUid()); - } - - @Override - @Transactional - public void unassignThread(ThreadEntity thread) { - String currentAgentUid = "";// thread.getAgentUid(); - if (currentAgentUid != null) { - // 更新客服工作负载 - AgentEntity agent = agentRepository.findByUid(currentAgentUid) - .orElseThrow(() -> new RuntimeException("Agent not found")); - agent.decreaseThreadCount(); - agentRepository.save(agent); - } - - // 更新会话信息 - // thread.setAgentUid(null); - // thread.setStatus("unassigned"); - threadRepository.save(thread); - - log.info("Thread {} unassigned from agent {}", thread.getUid(), currentAgentUid); - } - - @Override - @Transactional - public boolean reassignThread(ThreadEntity thread) { - try { - // 获取可用客服 - List availableAgents = agentService.getAvailableAgents(); - if (availableAgents.isEmpty()) { - // 无可用客服,加入排队队列 - queueService.enqueue(thread.getUid(), calculatePriority(thread)); - // thread.setStatus("queued"); - threadRepository.save(thread); - return false; - } - - // 取消当前分配 - unassignThread(thread); - - // 选择新客服并分配 - AgentEntity newAgent = selectAgent(availableAgents, thread); - assignToAgent(thread, newAgent); - - return true; - } catch (Exception e) { - log.error("Failed to reassign thread {}", thread.getUid(), e); - // 异常情况加入队列 - queueService.enqueue(thread.getUid(), 0); - // thread.setStatus("queued"); - threadRepository.save(thread); - return false; - } - } -} \ No newline at end of file diff --git a/modules/service/src/main/java/com/bytedesk/service/visitor_thread/VisitorThreadService.java b/modules/service/src/main/java/com/bytedesk/service/visitor_thread/VisitorThreadService.java index 0a7bafa892..a4f9ea9b27 100644 --- a/modules/service/src/main/java/com/bytedesk/service/visitor_thread/VisitorThreadService.java +++ b/modules/service/src/main/java/com/bytedesk/service/visitor_thread/VisitorThreadService.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-06-29 13:08:52 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2024-12-16 15:35:09 + * @LastEditTime: 2024-12-19 15:12:40 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -59,8 +59,6 @@ public class VisitorThreadService private final ThreadRestService threadService; - // private final ThreadStateService threadStateService; - private final ModelMapper modelMapper; private final UidUtils uidUtils;