diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDAgentService.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDAgentService.java new file mode 100644 index 00000000..4e5847e4 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDAgentService.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.acd.visitor.ACDVisAllocatorMw; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.model.*; +import com.chatopera.cc.proxy.AgentUserProxy; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ACDAgentService { + private final static Logger logger = LoggerFactory.getLogger(ACDAgentService.class); + + @Autowired + private ACDVisAllocatorMw acdAgentAllocatorMw; + + @Autowired + private AgentUserProxy agentUserProxy; + + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDQueueService acdQueueService; + + /** + * 为访客分配坐席 + * + * @param agentUser + */ + @SuppressWarnings("unchecked") + public AgentService allotAgent( + final AgentUser agentUser, + final String orgi) { + /** + * 查询条件,当前在线的 坐席,并且 未达到最大 服务人数的坐席 + */ + List agentStatusList = acdAgentAllocatorMw.filterOutAvailableAgentStatus(agentUser, orgi); + + /** + * 处理ACD 的 技能组请求和 坐席请求 + */ + AgentStatus agentStatus = null; + AgentService agentService = null; //放入缓存的对象 + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + if (agentStatusList.size() > 0) { + agentStatus = agentStatusList.get(0); + if (agentStatus.getUsers() >= sessionConfig.getMaxuser()) { + agentStatus = null; + /** + * 判断当前有多少人排队中 , 分三种情况:1、请求技能组的,2、请求坐席的,3,默认请求的 + * + */ + } + } + + try { + agentService = acdAgentAllocatorMw.processAgentService(agentStatus, agentUser, orgi, false, sessionConfig); + // 处理结果:进入排队队列 + if (StringUtils.equals(MainContext.AgentUserStatusEnum.INQUENE.toString(), agentService.getStatus())) { + agentService.setQueneindex( + acdQueueService.getQueueIndex(agentUser.getAgentno(), orgi, agentUser.getSkill())); + } + } catch (Exception ex) { + logger.warn("[allotAgent] exception: ", ex); + } + agentUserProxy.broadcastAgentsStatus( + orgi, "user", agentService != null && agentService.getStatus().equals( + MainContext.AgentUserStatusEnum.INSERVICE.toString()) ? "inservice" : "inquene", + agentUser.getId() + ); + return agentService; + } + + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDChatbotService.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDChatbotService.java new file mode 100644 index 00000000..ec941f68 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDChatbotService.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.model.AgentService; +import com.chatopera.cc.model.AgentUser; +import com.chatopera.cc.persistence.repository.AgentServiceRepository; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class ACDChatbotService { + private final static Logger logger = LoggerFactory.getLogger(ACDChatbotService.class); + + @Autowired + private AgentServiceRepository agentServiceRes; + + /** + * 为访客分配机器人客服, ACD策略,此处 AgentStatus 是建议 的 坐席, 如果启用了 历史服务坐席 优先策略, 则会默认检查历史坐席是否空闲,如果空闲,则分配,如果不空闲,则 分配当前建议的坐席 + * + * @param agentUser + * @param orgi + * @return + * @throws Exception + */ + public AgentService processChatbotService(final String botName, final AgentUser agentUser, final String orgi) { + AgentService agentService = new AgentService(); //放入缓存的对象 + Date now = new Date(); + if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + agentService = agentServiceRes.findByIdAndOrgi(agentUser.getAgentserviceid(), orgi); + agentService.setEndtime(now); + if (agentService.getServicetime() != null) { + agentService.setSessiontimes(System.currentTimeMillis() - agentService.getServicetime().getTime()); + } + agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + } else { + agentService.setServicetime(now); + agentService.setLogindate(now); + agentService.setOrgi(orgi); + agentService.setOwner(agentUser.getContextid()); + agentService.setSessionid(agentUser.getSessionid()); + agentService.setRegion(agentUser.getRegion()); + agentService.setUsername(agentUser.getUsername()); + agentService.setChannel(agentUser.getChannel()); + if (botName != null) { + agentService.setAgentusername(botName); + } + + if (StringUtils.isNotBlank(agentUser.getContextid())) { + agentService.setContextid(agentUser.getContextid()); + } else { + agentService.setContextid(agentUser.getSessionid()); + } + + agentService.setUserid(agentUser.getUserid()); + agentService.setAiid(agentUser.getAgentno()); + agentService.setAiservice(true); + agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); + + agentService.setAppid(agentUser.getAppid()); + agentService.setLeavemsg(false); + } + + agentServiceRes.save(agentService); + return agentService; + } + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDComposeContext.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDComposeContext.java new file mode 100644 index 00000000..c4c25d27 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDComposeContext.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.model.*; +import com.chatopera.cc.socketio.message.Message; +import com.chatopera.cc.util.IP; + +public class ACDComposeContext extends Message { + + // 技能组及渠道 + private String orgi; + private String organid; + private Organ organ; + private String appid; + private String channel; + private SNSAccount snsAccount; + private String sessionid; + + // 策略 + private SessionConfig sessionConfig; + // 坐席报告 + private AgentReport agentReport; + + // 机器人客服 + private String aiid; + private boolean isAi; + + // 是否是邀请 + private boolean isInvite; + + private User agent; + private String agentno; + private String agentUserId; + + private String agentServiceId; + + private AgentUser agentUser; + + private AgentService agentService; + + // 访客 + private String onlineUserId; + private OnlineUser onlineUser; + private String onlineUserNickname; + private String onlineUserHeadimgUrl; + + // 其它信息 + private IP ipdata; + private String initiator; + private String title; + private String url; + private String browser; + private String osname; + private String traceid; + private String ownerid; + private String ip; + + public String getOrganid() { + return organid; + } + + public void setOrganid(String organid) { + this.organid = organid; + } + + public Organ getOrgan() { + return organ; + } + + public void setOrgan(Organ organ) { + this.organ = organ; + } + + public String getAppid() { + return appid; + } + + public void setAppid(String appid) { + this.appid = appid; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public SNSAccount getSnsAccount() { + return snsAccount; + } + + public void setSnsAccount(SNSAccount snsAccount) { + this.snsAccount = snsAccount; + } + + public SessionConfig getSessionConfig() { + return sessionConfig; + } + + public void setSessionConfig(SessionConfig sessionConfig) { + this.sessionConfig = sessionConfig; + } + + public String getAiid() { + return aiid; + } + + public void setAiid(String aiid) { + this.aiid = aiid; + } + + public boolean isAi() { + return isAi; + } + + public void setAi(boolean ai) { + isAi = ai; + } + + public boolean isInvite() { + return isInvite; + } + + public void setInvite(boolean invite) { + isInvite = invite; + } + + public User getAgent() { + return agent; + } + + public void setAgent(User agent) { + this.agent = agent; + } + + public String getAgentno() { + return agentno; + } + + public void setAgentno(String agentno) { + this.agentno = agentno; + } + + public String getAgentUserId() { + return agentUserId; + } + + public void setAgentUserId(String agentUserId) { + this.agentUserId = agentUserId; + } + + public String getOnlineUserId() { + return onlineUserId; + } + + public void setOnlineUserId(String onlineUserId) { + this.onlineUserId = onlineUserId; + } + + public String getAgentServiceId() { + return agentServiceId; + } + + public void setAgentServiceId(String agentServiceId) { + this.agentServiceId = agentServiceId; + } + + public AgentUser getAgentUser() { + return agentUser; + } + + public void setAgentUser(AgentUser agentUser) { + this.agentUser = agentUser; + } + + public OnlineUser getOnlineUser() { + return onlineUser; + } + + public void setOnlineUser(OnlineUser onlineUser) { + this.onlineUser = onlineUser; + } + + public AgentService getAgentService() { + return agentService; + } + + public void setAgentService(AgentService agentService) { + this.agentService = agentService; + } + + public String getSessionid() { + return sessionid; + } + + public void setSessionid(String sessionid) { + this.sessionid = sessionid; + } + + public String getOnlineUserNickname() { + return onlineUserNickname; + } + + public void setOnlineUserNickname(String onlineUserNickname) { + this.onlineUserNickname = onlineUserNickname; + } + + public IP getIpdata() { + return ipdata; + } + + public void setIpdata(IP ipdata) { + this.ipdata = ipdata; + } + + public String getInitiator() { + return initiator; + } + + public void setInitiator(String initiator) { + this.initiator = initiator; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOsname() { + return osname; + } + + public void setOsname(String osname) { + this.osname = osname; + } + + public String getTraceid() { + return traceid; + } + + public void setTraceid(String traceid) { + this.traceid = traceid; + } + + public String getOwnerid() { + return ownerid; + } + + public void setOwnerid(String ownerid) { + this.ownerid = ownerid; + } + + public String getOrgi() { + return orgi; + } + + public void setOrgi(String orgi) { + this.orgi = orgi; + } + + public String getOnlineUserHeadimgUrl() { + return onlineUserHeadimgUrl; + } + + public void setOnlineUserHeadimgUrl(String onlineUserHeadimgUrl) { + this.onlineUserHeadimgUrl = onlineUserHeadimgUrl; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public AgentReport getAgentReport() { + return agentReport; + } + + public void setAgentReport(AgentReport agentReport) { + this.agentReport = agentReport; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDMessageHelper.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDMessageHelper.java new file mode 100644 index 00000000..65724d17 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDMessageHelper.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.model.AgentService; +import com.chatopera.cc.model.SessionConfig; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ACDMessageHelper { + private final static Logger logger = LoggerFactory.getLogger(ACDMessageHelper.class); + + @Autowired + private ACDPolicyService acdPolicyService; + + /** + * 通知消息内容:分配到坐席 + * + * @param agentService + * @param channel + * @param orgi + * @return + */ + public String getSuccessMessage(AgentService agentService, String channel, String orgi) { + String queneTip = "" + agentService.getAgentusername() + ""; + if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { + queneTip = agentService.getAgentusername(); + } + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + String successMsg = "坐席分配成功," + queneTip + "为您服务。"; + if (StringUtils.isNotBlank(sessionConfig.getSuccessmsg())) { + successMsg = sessionConfig.getSuccessmsg().replaceAll("\\{agent\\}", queneTip); + } + return successMsg; + } + + /** + * 通知消息内容:和坐席断开 + * + * @param channel + * @param orgi + * @return + */ + public String getServiceFinishMessage(String channel, String orgi) { + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + String queneTip = "坐席已断开和您的对话"; + if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) { + queneTip = sessionConfig.getFinessmsg(); + } + return queneTip; + } + + + /** + * 通知消息内容:和坐席断开,刷新页面 + * + * @param channel + * @param orgi + * @return + */ + public String getServiceOffMessage(String channel, String orgi) { + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + String queneTip = "坐席已断开和您的对话,刷新页面为您分配新的坐席"; + if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) { + queneTip = sessionConfig.getFinessmsg(); + } + return queneTip; + } + + public String getNoAgentMessage(int queneIndex, String channel, String orgi) { + if (queneIndex < 0) { + queneIndex = 0; + } + String queneTip = "" + queneIndex + ""; + if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { + queneTip = String.valueOf(queneIndex); + } + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + String noAgentTipMsg = "坐席全忙,已进入等待队列,您也可以在其他时间再来咨询。"; + if (StringUtils.isNotBlank(sessionConfig.getNoagentmsg())) { + noAgentTipMsg = sessionConfig.getNoagentmsg().replaceAll("\\{num\\}", queneTip); + } + return noAgentTipMsg; + } + + public String getQueneMessage(int queneIndex, String channel, String orgi) { + + String queneTip = "" + queneIndex + ""; + if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { + queneTip = String.valueOf(queneIndex); + } + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + String agentBusyTipMsg = "正在排队,请稍候,在您之前,还有 " + queneTip + " 位等待用户。"; + if (StringUtils.isNotBlank(sessionConfig.getAgentbusymsg())) { + agentBusyTipMsg = sessionConfig.getAgentbusymsg().replaceAll("\\{num\\}", queneTip); + } + return agentBusyTipMsg; + } + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDPolicyService.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDPolicyService.java new file mode 100644 index 00000000..fcc9b83c --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDPolicyService.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.SessionConfig; +import com.chatopera.cc.persistence.repository.SessionConfigRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 坐席自动分配策略集 + */ +@Component +public class ACDPolicyService { + private final static Logger logger = LoggerFactory.getLogger(ACDPolicyService.class); + + @Autowired + private Cache cache; + + @Autowired + private SessionConfigRepository sessionConfigRes; + + /** + * 载入坐席 ACD策略配置 + * + * @return + */ + @SuppressWarnings("unchecked") + public List initSessionConfigList() { + List sessionConfigList; + if ((sessionConfigList = cache.findOneSessionConfigListByOrgi(MainContext.SYSTEM_ORGI)) == null) { + sessionConfigList = sessionConfigRes.findAll(); + if (sessionConfigList != null && sessionConfigList.size() > 0) { + cache.putSessionConfigListByOrgi(sessionConfigList, MainContext.SYSTEM_ORGI); + } + } + return sessionConfigList; + } + + /** + * 载入坐席 ACD策略配置 + * + * @param orgi + * @return + */ + public SessionConfig initSessionConfig(final String orgi) { + SessionConfig sessionConfig; + if ((sessionConfig = cache.findOneSessionConfigByOrgi(orgi)) == null) { + sessionConfig = sessionConfigRes.findByOrgi(orgi); + if (sessionConfig == null) { + sessionConfig = new SessionConfig(); + } else { + cache.putSessionConfigByOrgi(sessionConfig, orgi); + } + } + return sessionConfig; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDQueueService.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDQueueService.java new file mode 100644 index 00000000..306f6fe0 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDQueueService.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd; + +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.AgentUser; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class ACDQueueService { + private final static Logger logger = LoggerFactory.getLogger(ACDQueueService.class); + + + @Autowired + private Cache cache; + + @SuppressWarnings("unchecked") + public int getQueueIndex(String agent, String orgi, String skill) { + int queneUsers = 0; + Map map = cache.getAgentUsersInQueByOrgi(orgi); + + for (Map.Entry entry : map.entrySet()) { + if (StringUtils.isNotBlank(skill)) { + if (StringUtils.equals(entry.getValue().getSkill(), skill)) { + queneUsers++; + } + continue; + } else { + if (StringUtils.isNotBlank(agent)) { + if (StringUtils.equals(entry.getValue().getAgentno(), agent)) { + queneUsers++; + } + continue; + } else { + queneUsers++; + } + } + } + return queneUsers; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDServiceRouter.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDServiceRouter.java new file mode 100644 index 00000000..eae4c57e --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDServiceRouter.java @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.chatopera.cc.acd; + +import com.chatopera.cc.acd.agent.ACDAgentMw1; +import com.chatopera.cc.acd.visitor.*; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.cache.RedisCommand; +import com.chatopera.cc.cache.RedisKey; +import com.chatopera.cc.exception.CSKefuException; +import com.chatopera.cc.model.*; +import com.chatopera.cc.peer.PeerSyncIM; +import com.chatopera.cc.persistence.repository.*; +import com.chatopera.cc.proxy.AgentUserProxy; +import com.chatopera.cc.socketio.client.NettyClients; +import com.chatopera.cc.socketio.message.Message; +import com.chatopera.cc.util.IP; +import com.chatopera.cc.util.SerializeUtil; +import com.chatopera.compose4j.Composer; +import com.chatopera.compose4j.exception.Compose4jRuntimeException; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Automatic Call Distribution Main Entry + */ +@SuppressWarnings("deprecation") +@Component +public class ACDServiceRouter { + private final static Logger logger = LoggerFactory.getLogger(ACDServiceRouter.class); + + // Redis缓存: 缓存的底层实现接口 + @Autowired + private RedisCommand redisCommand; + + // 缓存管理:高级缓存实现接口 + @Autowired + private Cache cache; + + // 在线访客与坐席关联表 + @Autowired + private AgentUserRepository agentUserRes; + + // 在线访客 + @Autowired + private OnlineUserRepository onlineUserRes; + + // 坐席服务记录 + @Autowired + private AgentServiceRepository agentServiceRes; + + + @Autowired + private AgentUserProxy agentUserProxy; + + // 坐席服务任务 + @Autowired + private AgentUserTaskRepository agentUserTaskRes; + + // 机器人坐席 + @Autowired + private ACDChatbotService acdChatbotService; + + // 坐席状态 + @Autowired + private AgentStatusRepository agentStatusRes; + + // 消息工厂 + @Autowired + private ACDMessageHelper acdMessageHelper; + + // 坐席服务 + @Autowired + private ACDAgentService acdAgentService; + + // 消息分发 + @Autowired + private PeerSyncIM peerSyncIM; + + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDQueueService acdQueueService; + + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + /** + * 为坐席安排访客 + */ + private Composer agentPipeline; + + @Autowired + private ACDAgentMw1 acdAgentMw1; + + /** + * 为访客安排坐席 + */ + private Composer visitorPipeline; + + @Autowired + private ACDVisBodyParserMw acdVisBodyParserMw; + + @Autowired + private ACDVisBindingMw acdVisBindingMw; + + @Autowired + private ACDVisSessionCfgMw acdVisSessionCfgMw; + + @Autowired + private ACDVisServiceMw acdVisServiceMw; + + @Autowired + private ACDVisAllocatorMw acdVisAllocatorMw; + + @PostConstruct + private void setup() { + logger.info("[setup] setup ACD Algorithm Service ..."); + + setUpAgentPipeline(); + setUpVisitorPipeline(); + } + + /** + * 建立坐席处理管道 + */ + private void setUpAgentPipeline() { + agentPipeline = new Composer<>(); + agentPipeline.use(acdAgentMw1); + } + + + /** + * 建立访客处理管道 + */ + private void setUpVisitorPipeline() { + visitorPipeline = new Composer<>(); + + /** + * 1) 设置基本信息 + */ + visitorPipeline.use(acdVisBodyParserMw); + + /** + * 1) 绑定技能组或坐席(包括邀请时的坐席) + */ + visitorPipeline.use(acdVisBindingMw); + + /** + * 1) 坐席配置:工作时间段,有无就绪在线坐席 + * + */ + visitorPipeline.use(acdVisSessionCfgMw); + + /** + * 1)选择坐席,确定AgentService + */ + visitorPipeline.use(acdVisServiceMw); + + /** + * 1)根据策略筛选坐席 + */ + visitorPipeline.use(acdVisAllocatorMw); + } + + + /** + * 为坐席批量分配用户 + * + * @param agentno + * @param orgi + */ + @SuppressWarnings("unchecked") + public void allotVisitors(String agentno, String orgi) { + // 获得目标坐席的状态 + AgentStatus agentStatus = SerializeUtil.deserialize( + redisCommand.getHashKV(RedisKey.getAgentStatusReadyHashKey(orgi), agentno)); + + if (agentStatus == null) { + logger.warn("[allotAgent] can not find AgentStatus for agentno {}", agentno); + return; + } + + // 获得所有待服务访客的列表 + Map pendingAgentUsers = cache.getAgentUsersInQueByOrgi(orgi); + + for (Map.Entry entry : pendingAgentUsers.entrySet()) { + AgentUser agentUser = entry.getValue(); + boolean process = false; + + if ((StringUtils.equals(agentUser.getAgentno(), agentno))) { + // 待服务的访客指定了该坐席 + process = true; + } else { + if (agentStatus != null && + agentStatus.getSkills() != null && + agentStatus.getSkills().size() > 0) { + // 目标坐席有状态,并且坐席属于某技能组 + if ((StringUtils.isBlank(agentUser.getAgentno()) && + StringUtils.isBlank(agentUser.getSkill()))) { + // 待服务的访客还没有指定坐席,并且也没有绑定技能组 + process = true; + } else { + if (StringUtils.isBlank(agentUser.getAgentno()) && + agentStatus.getSkills().containsKey(agentUser.getSkill())) { + // 待服务的访客还没有指定坐席,并且指定的技能组和该坐席的技能组一致 + process = true; + } + } + } else { + // 目标坐席没有状态,或该目标坐席有状态但是没有属于任何一个技能组 + if (StringUtils.isBlank(agentUser.getAgentno()) && + StringUtils.isBlank(agentUser.getSkill())) { + // 待服务访客没有指定坐席,并且没有指定技能组 + process = true; + } + } + } + + if (!process) { + continue; + } + + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + long maxusers = sessionConfig == null ? Constants.AGENT_STATUS_MAX_USER : sessionConfig.getMaxuser(); + if (agentStatus.getUsers() < maxusers) { //坐席未达到最大咨询访客数量 + // 从排队队列移除 + cache.deleteAgentUserInqueByAgentUserIdAndOrgi(agentUser.getUserid(), orgi); + + // 下面开始处理其加入到服务中的队列 + try { + AgentService agentService = acdVisAllocatorMw.processAgentService( + agentStatus, agentUser, orgi, false, sessionConfig); + + // 处理完成得到 agentService + Message outMessage = new Message(); + outMessage.setMessage(acdMessageHelper.getSuccessMessage( + agentService, + agentUser.getChannel(), + orgi)); + outMessage.setMessageType(MainContext.MediaType.TEXT.toString()); + outMessage.setCalltype(MainContext.CallType.IN.toString()); + outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); + + if (StringUtils.isNotBlank(agentUser.getUserid())) { + outMessage.setAgentUser(agentUser); + outMessage.setChannelMessage(agentUser); + + // 向访客推送消息 + peerSyncIM.send( + MainContext.ReceiverType.VISITOR, + MainContext.ChannelType.toValue(agentUser.getChannel()), agentUser.getAppid(), + MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true + ); + + // 向坐席推送消息 + peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, + agentUser.getAppid(), + MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true); + } + } catch (Exception ex) { + logger.warn("[allotAgent] fail to process service", ex); + } + } else { + logger.info("[allotAgent] agentno {} reach the max users limit", agentno); + break; + } + } + agentUserProxy.broadcastAgentsStatus(orgi, "agent", "success", agentno); + } + + /** + * 访客服务结束 + * + * @param agentUser + * @param orgi + * @throws Exception + */ + public void serviceFinish(final AgentUser agentUser, final String orgi) { + if (agentUser != null) { + // 获得坐席状态 + AgentStatus agentStatus = null; + if (StringUtils.equals(MainContext.AgentUserStatusEnum.INSERVICE.toString(), agentUser.getStatus()) && + agentUser.getAgentno() != null) { + agentStatus = cache.findOneAgentStatusByAgentnoAndOrig(agentUser.getAgentno(), orgi); + } + + // 设置新AgentUser的状态 + agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + if (agentUser.getServicetime() != null) { + agentUser.setSessiontimes(System.currentTimeMillis() - agentUser.getServicetime().getTime()); + } + + // 从缓存中删除agentUser缓存 + agentUserRes.save(agentUser); + + final SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + + // 坐席服务 + AgentService service = null; + if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + service = agentServiceRes.findByIdAndOrgi(agentUser.getAgentserviceid(), agentUser.getOrgi()); + } else if (agentStatus != null) { + // 该访客没有和坐席对话,因此没有 AgentService + // 当做留言处理,创建一个新的 AgentService + service = acdVisAllocatorMw.processAgentService(agentStatus, agentUser, orgi, true, sessionConfig); + } + + if (service != null) { + service.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + service.setEndtime(new Date()); + if (service.getServicetime() != null) { + service.setSessiontimes(System.currentTimeMillis() - service.getServicetime().getTime()); + } + + final List agentUserTaskList = agentUserTaskRes.findByIdAndOrgi( + agentUser.getId(), agentUser.getOrgi()); + if (agentUserTaskList.size() > 0) { + final AgentUserTask agentUserTask = agentUserTaskList.get(0); + service.setAgentreplyinterval(agentUserTask.getAgentreplyinterval()); + service.setAgentreplytime(agentUserTask.getAgentreplytime()); + service.setAvgreplyinterval(agentUserTask.getAvgreplyinterval()); + service.setAvgreplytime(agentUserTask.getAvgreplytime()); + + service.setUserasks(agentUserTask.getUserasks()); + service.setAgentreplys(agentUserTask.getAgentreplys()); + + // 开启了质检,并且是有效对话 + if (sessionConfig.isQuality()) { + // 未分配质检任务 + service.setQualitystatus(MainContext.QualityStatusEnum.NODIS.toString()); + } + } + + /** + * 启用了质检任务,开启质检 + */ + if ((!sessionConfig.isQuality()) || service.getUserasks() == 0) { + // 未开启质检 或无效对话无需质检 + service.setQualitystatus(MainContext.QualityStatusEnum.NO.toString()); + } + agentServiceRes.save(service); + } + + /** + * 发送到访客端的通知 + */ + switch (MainContext.ChannelType.toValue(agentUser.getChannel())) { + case WEBIM: + // WebIM 发送对话结束事件 + // 向访客发送消息 + Message outMessage = new Message(); + outMessage.setAgentStatus(agentStatus); + outMessage.setMessage(acdMessageHelper.getServiceFinishMessage(agentUser.getChannel(), orgi)); + outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString()); + outMessage.setCalltype(MainContext.CallType.IN.toString()); + outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); + outMessage.setAgentUser(agentUser); + + // 向访客发送消息 + peerSyncIM.send( + MainContext.ReceiverType.VISITOR, + MainContext.ChannelType.toValue(agentUser.getChannel()), agentUser.getAppid(), + MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true + ); + + if (agentStatus != null) { + // 坐席在线,通知结束会话 + outMessage.setChannelMessage(agentUser); + outMessage.setAgentUser(agentUser); + peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, + agentUser.getAppid(), + MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true); + } + break; + case PHONE: + // 语音渠道,强制发送 + logger.info("[serviceFinish] send notify to callout channel agentno {}", agentUser.getAgentno()); + NettyClients.getInstance().sendCalloutEventMessage( + agentUser.getAgentno(), MainContext.MessageType.END.toString(), agentUser); + break; + default: + logger.info( + "[serviceFinish] ignore notify agent service end for channel {}, agent user id {}", + agentUser.getChannel(), agentUser.getId()); + } + + // 更新访客的状态为可以接收邀请 + final OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi( + agentUser.getUserid(), agentUser.getOrgi()); + if (onlineUser != null) { + onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString()); + onlineUserRes.save(onlineUser); + logger.info( + "[online] onlineUser id {}, status {}, invite status {}", onlineUser.getId(), + onlineUser.getStatus(), onlineUser.getInvitestatus()); + } + + // 当前访客服务已经结束,为坐席寻找新访客 + if (agentStatus != null) { + long maxusers = sessionConfig != null ? sessionConfig.getMaxuser() : Constants.AGENT_STATUS_MAX_USER; + if ((agentStatus.getUsers() - 1) < maxusers) { + allotVisitors(agentStatus.getAgentno(), orgi); + } + } + agentUserProxy.broadcastAgentsStatus(orgi, "end", "success", agentUser != null ? agentUser.getId() : null); + } else { + logger.info("[serviceFinish] orgi {}, invalid agent user, should not be null", orgi); + } + } + + + /** + * 撤退一个坐席 + * 1)将该坐席状态置为"非就绪" + * 2) 将该坐席的访客重新分配给其它坐席 + * + * @param orgi + * @param agentno + * @return 有没有成功将所有其服务的访客都分配出去 + */ + public boolean withdrawAgent(final String orgi, final String agentno) { + // 先将该客服切换到非就绪状态 + final AgentStatus agentStatus = cache.findOneAgentStatusByAgentnoAndOrig(agentno, orgi); + if (agentStatus != null) { + agentStatus.setBusy(false); + agentStatus.setUpdatetime(new Date()); + agentStatus.setStatus(MainContext.AgentStatusEnum.NOTREADY.toString()); + agentStatusRes.save(agentStatus); + cache.putAgentStatusByOrgi(agentStatus, orgi); + } + + // 然后将该坐席的访客分配给其它坐席 + // 获得该租户在线的客服的多少 + // TODO 对于agentUser的技能组过滤,在下面再逐个考虑? + // 该信息同样也包括当前用户 + List agentUsers = cache.findInservAgentUsersByAgentnoAndOrgi(agentno, orgi); + int sz = agentUsers.size(); + for (final AgentUser x : agentUsers) { + try { + // TODO 此处没有考虑遍历过程中,系统中坐席的服务访客的信息实际上是变化的 + // 可能会发生maxusers超过设置的情况,如果做很多检查,会带来一定一系统开销 + // 因为影响不大,放弃实时的检查 + acdAgentService.allotAgent(x, x.getOrgi()); + // 因为重新分配该访客,将其从撤离的坐席中服务集合中删除 + // 此处类似于 Transfer + redisCommand.removeSetVal( + RedisKey.getInServAgentUsersByAgentnoAndOrgi(agentno, orgi), x.getUserid()); + sz--; + } catch (Exception e) { + logger.warn("[withdrawAgent] throw error:", e); + } + } + + if (sz == 0) { + logger.info("[withdrawAgent] after re-allotAgent, the agentUsers size is {} for agentno {}", sz, agentno); + } else { + logger.warn("[withdrawAgent] after re-allotAgent, the agentUsers size is {} for agentno {}", sz, agentno); + } + + return sz == 0; + } + + /** + * 邀请访客进入当前对话,如果当前操作的 坐席是已就绪状态,则直接加入到当前坐席的 + * 对话列表中,如果未登录,则分配给其他坐席 + * + * @param agentno + * @param agentUser + * @param orgi + * @return + * @throws Exception + */ + public AgentService allotAgentForInvite( + final String agentno, + final AgentUser agentUser, + final String orgi + ) throws Exception { + AgentStatus agentStatus = cache.findOneAgentStatusByAgentnoAndOrig(agentno, orgi); + AgentService agentService; + if (agentStatus != null) { + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); + agentService = acdVisAllocatorMw.processAgentService(agentStatus, agentUser, orgi, false, sessionConfig); + agentUserProxy.broadcastAgentsStatus(orgi, "invite", "success", agentno); + + /** + * 通知坐席新的访客邀请成功 + */ + Message outMessage = new Message(); + outMessage.setAgentUser(agentUser); + outMessage.setChannelMessage(agentUser); + peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, + agentUser.getAppid(), + MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true); + } else { + agentService = acdAgentService.allotAgent(agentUser, orgi); + } + return agentService; + } + + + /** + * 删除AgentUser + * 包括数据库记录及缓存信息 + * + * @param agentUser + * @param orgi + * @return + */ + public void deleteAgentUser(final AgentUser agentUser, final String orgi) throws CSKefuException { + logger.info("[deleteAgentUser] userId {}, orgi {}", agentUser.getUserid(), orgi); + + if (agentUser == null || agentUser.getId() == null) { + throw new CSKefuException("Invalid agentUser info"); + } + + if (!StringUtils.equals(MainContext.AgentUserStatusEnum.END.toString(), agentUser.getStatus())) { + /** + * 未结束聊天,先结束对话,然后删除记录 + */ + // 删除缓存 + serviceFinish(agentUser, orgi); + } + + // 删除数据库里的AgentUser记录 + agentUserRes.delete(agentUser); + } + + + /** + * 为新增加的访客会话分配坐席和开启访客与坐席的对话 + * + * @param onlineUserId + * @param nickname + * @param orgi + * @param session + * @param appid + * @param ip + * @param osname + * @param browser + * @param headimg + * @param ipdata + * @param channel + * @param skill + * @param agent + * @param title + * @param url + * @param traceid + * @param ownerid + * @param initiator + * @return + * @throws Exception + */ + public Message allocateAgentService( + final String onlineUserId, + final String nickname, + final String orgi, + final String session, + final String appid, + final String ip, + final String osname, + final String browser, + final String headimg, + final IP ipdata, + final String channel, + final String skill, + final String agent, + final String title, + final String url, + final String traceid, + final String ownerid, + final boolean isInvite, + final String initiator) { + logger.info( + "[allocateAgentService] user {}, appid {}, agent {}, skill {}, nickname {}, initiator {}, isInvite {}", + onlineUserId, + appid, + agent, + skill, + nickname, initiator, isInvite); + + // 坐席服务请求,分配 坐席 + Message result = new Message(); + + final ACDComposeContext ctx = new ACDComposeContext(); + ctx.setOnlineUserId(onlineUserId); + ctx.setOnlineUserNickname(nickname); + ctx.setOrganid(skill); + ctx.setOrgi(orgi); + ctx.setChannel(channel); + ctx.setAgentno(agent); + ctx.setBrowser(browser); + ctx.setOsname(osname); + ctx.setAppid(appid); + ctx.setTitle(title); + ctx.setSessionid(session); + ctx.setUrl(url); + ctx.setOnlineUserHeadimgUrl(headimg); + ctx.setTraceid(traceid); + ctx.setOwnerid(ownerid); + ctx.setInitiator(initiator); + ctx.setIpdata(ipdata); + ctx.setIp(ip); + ctx.setInvite(isInvite); + + try { + visitorPipeline.handle(ctx); + result = (Message) ctx; + } catch (Compose4jRuntimeException e) { + logger.error("[allocateAgentService] error", e); + } + + return result; + } + + + public ACDPolicyService getAcdPolicyService() { + return acdPolicyService; + } + + public ACDMessageHelper getAcdMessageHelper() { + return acdMessageHelper; + } + + public ACDAgentService getAcdAgentService() { + return acdAgentService; + } + + public ACDChatbotService getAcdChatbotService() { + return acdChatbotService; + } + + public ACDQueueService getAcdQueueService() { + return acdQueueService; + } + + public ACDWorkMonitor getAcdWorkMonitor() { + return acdWorkMonitor; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDWorkMonitor.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDWorkMonitor.java new file mode 100644 index 00000000..fc8df5b8 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/ACDWorkMonitor.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.chatopera.cc.acd; + +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.AgentReport; +import com.chatopera.cc.model.AgentStatus; +import com.chatopera.cc.model.WorkMonitor; +import com.chatopera.cc.persistence.repository.WorkMonitorRepository; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Component +public class ACDWorkMonitor { + private final static Logger logger = LoggerFactory.getLogger(ACDWorkMonitor.class); + + @Autowired + private WorkMonitorRepository workMonitorRes; + + @Autowired + private Cache cache; + + /** + * 获得 当前服务状态 + * + * @param orgi + * @return + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public AgentReport getAgentReport(String orgi) { + return getAgentReport(null, orgi); + } + + /** + * 获得一个技能组的坐席状态 + * + * @param organ + * @param orgi + * @return + */ + public AgentReport getAgentReport(String organ, String orgi) { + /** + * 统计当前在线的坐席数量 + */ + AgentReport report = new AgentReport(); + + Map readys = cache.getAgentStatusReadyByOrig(orgi); + int readyNum = 0; + int busyNum = 0; + + for (Map.Entry entry : readys.entrySet()) { + if (organ == null) { + readyNum++; + if (entry.getValue().isBusy()) { + busyNum++; + } + continue; + } + + if (entry.getValue().getSkills() != null && + entry.getValue().getSkills().containsKey(organ)) { + readyNum++; + if (entry.getValue().isBusy()) { + busyNum++; + } + + } + } + report.setAgents(readyNum); + report.setBusy(busyNum); + report.setOrgi(orgi); + + /** + * 统计当前服务中的用户数量 + */ + // 服务中 + report.setUsers(cache.getInservAgentUsersSizeByOrgi(orgi)); + // 等待中 + report.setInquene(cache.getInqueAgentUsersSizeByOrgi(orgi)); + + // DEBUG +// logger.info( +// "[getAgentReport] orgi {}, organ {}, agents {}, busy {}, users {}, inqueue {}", orgi, organ, +// report.getAgents(), report.getBusy(), report.getUsers(), report.getInquene() +// ); + return report; + } + + /** + * @param agent 坐席 + * @param userid 用户ID + * @param status 工作状态,也就是上一个状态 + * @param current 下一个工作状态 + * @param worktype 类型 : 语音OR 文本 + * @param orgi + * @param lasttime + */ + public void recordAgentStatus( + String agent, + String username, + String extno, + boolean admin, + String userid, + String status, + String current, + String worktype, + String orgi, + Date lasttime + ) { + WorkMonitor workMonitor = new WorkMonitor(); + if (StringUtils.isNotBlank(agent) && StringUtils.isNotBlank(status)) { + workMonitor.setAgent(agent); + workMonitor.setAgentno(agent); + workMonitor.setStatus(status); + workMonitor.setAdmin(admin); + workMonitor.setUsername(username); + workMonitor.setExtno(extno); + workMonitor.setWorktype(worktype); + if (lasttime != null) { + workMonitor.setDuration((int) (System.currentTimeMillis() - lasttime.getTime()) / 1000); + } + if (status.equals(MainContext.AgentStatusEnum.BUSY.toString())) { + workMonitor.setBusy(true); + } + if (status.equals(MainContext.AgentStatusEnum.READY.toString())) { + int count = workMonitorRes.countByAgentAndDatestrAndStatusAndOrgi( + agent, MainUtils.simpleDateFormat.format(new Date()), + MainContext.AgentStatusEnum.READY.toString(), orgi + ); + if (count == 0) { + workMonitor.setFirsttime(true); + } + } + if (current.equals(MainContext.AgentStatusEnum.NOTREADY.toString())) { + List workMonitorList = workMonitorRes.findByOrgiAndAgentAndDatestrAndFirsttime( + orgi, agent, MainUtils.simpleDateFormat.format(new Date()), true); + if (workMonitorList.size() > 0) { + WorkMonitor firstWorkMonitor = workMonitorList.get(0); + if (firstWorkMonitor.getFirsttimes() == 0) { + firstWorkMonitor.setFirsttimes( + (int) (System.currentTimeMillis() - firstWorkMonitor.getCreatetime().getTime())); + workMonitorRes.save(firstWorkMonitor); + } + } + } + workMonitor.setCreatetime(new Date()); + workMonitor.setDatestr(MainUtils.simpleDateFormat.format(new Date())); + + workMonitor.setName(agent); + workMonitor.setOrgi(orgi); + workMonitor.setUserid(userid); + + workMonitorRes.save(workMonitor); + } + } + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/AutomaticServiceDist.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/AutomaticServiceDist.java deleted file mode 100644 index 439d2150..00000000 --- a/contact-center/app/src/main/java/com/chatopera/cc/acd/AutomaticServiceDist.java +++ /dev/null @@ -1,1223 +0,0 @@ -/* - * Copyright (C) 2017 优客服-多渠道客服系统 - * Modifications copyright (C) 2018-2019 Chatopera Inc, - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.chatopera.cc.acd; - -import com.chatopera.cc.basic.Constants; -import com.chatopera.cc.basic.MainContext; -import com.chatopera.cc.basic.MainUtils; -import com.chatopera.cc.cache.Cache; -import com.chatopera.cc.cache.RedisCommand; -import com.chatopera.cc.cache.RedisKey; -import com.chatopera.cc.exception.CSKefuException; -import com.chatopera.cc.model.*; -import com.chatopera.cc.peer.PeerSyncIM; -import com.chatopera.cc.persistence.es.ContactsRepository; -import com.chatopera.cc.persistence.repository.*; -import com.chatopera.cc.proxy.AgentAuditProxy; -import com.chatopera.cc.socketio.client.NettyClients; -import com.chatopera.cc.socketio.message.Message; -import com.chatopera.cc.util.SerializeUtil; -import com.chatopera.cc.util.WebIMReport; -import com.corundumstudio.socketio.SocketIONamespace; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * Automatic Call Distribution - */ -@SuppressWarnings("deprecation") -public class AutomaticServiceDist { - private final static Logger logger = LoggerFactory.getLogger(AutomaticServiceDist.class); - - // Redis缓存: 缓存的底层实现接口 - private static RedisCommand redisCommand; - - // 缓存管理:高级缓存实现接口 - private static Cache cache; - - // 在线访客与坐席关联表 - private static AgentUserRepository agentUserRes; - - // 在线访客 - private static OnlineUserRepository onlineUserRes; - - // 坐席服务记录 - private static AgentServiceRepository agentServiceRes; - - // 坐席服务任务 - private static AgentUserTaskRepository agentUserTaskRes; - - // 坐席状态报告 - private static AgentReportRepository agentReportRes; - - // 坐席状态 - private static AgentStatusRepository agentStatusRes; - - // 坐席配置 - private static SessionConfigRepository sessionConfigRes; - - //用户 - private static UserRepository UserRes; - - // 联系人 - private static ContactsRepository contactsRes; - - // 联系人会话关联表 - private static AgentUserContactsRepository agentUserContactsRes; - - // 会话监控 - private static AgentAuditProxy agentAuditProxy; - - - // 消息分发 - private static PeerSyncIM peerSyncIM; - - /** - * 载入坐席 ACD策略配置 - * - * @param orgi - * @return - */ - public static SessionConfig initSessionConfig(final String orgi) { - SessionConfig sessionConfig; - if ((sessionConfig = getCache().findOneSessionConfigByOrgi(orgi)) == null) { - sessionConfig = getSessionConfigRes().findByOrgi(orgi); - if (sessionConfig == null) { - sessionConfig = new SessionConfig(); - } else { - getCache().putSessionConfigByOrgi(sessionConfig, orgi); - } - } - return sessionConfig; - } - - /** - * 载入坐席 ACD策略配置 - * - * @return - */ - @SuppressWarnings("unchecked") - public static List initSessionConfigList() { - List sessionConfigList; - if ((sessionConfigList = getCache().findOneSessionConfigListByOrgi(MainContext.SYSTEM_ORGI)) == null) { - SessionConfigRepository sessionConfigRes = MainContext.getContext().getBean(SessionConfigRepository.class); - sessionConfigList = sessionConfigRes.findAll(); - if (sessionConfigList != null && sessionConfigList.size() > 0) { - getCache().putSessionConfigListByOrgi(sessionConfigList, MainContext.SYSTEM_ORGI); - } - } - return sessionConfigList; - } - - /** - * 获得 当前服务状态 - * - * @param orgi - * @return - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - public static AgentReport getAgentReport(String orgi) { - return AutomaticServiceDist.getAgentReport(null, orgi); - } - - /** - * 获得一个技能组的坐席状态 - * - * @param organ - * @param orgi - * @return - */ - public static AgentReport getAgentReport(String organ, String orgi) { - /** - * 统计当前在线的坐席数量 - */ - AgentReport report = new AgentReport(); - - Map readys = getCache().getAgentStatusReadyByOrig(orgi); - int readyNum = 0; - int busyNum = 0; - - for (Map.Entry entry : readys.entrySet()) { - if (organ == null) { - readyNum++; - if (entry.getValue().isBusy()) { - busyNum++; - } - continue; - } - - if (entry.getValue().getSkills() != null && - entry.getValue().getSkills().containsKey(organ)) { - readyNum++; - if (entry.getValue().isBusy()) { - busyNum++; - } - - } - } - report.setAgents(readyNum); - report.setBusy(busyNum); - report.setOrgi(orgi); - - /** - * 统计当前服务中的用户数量 - */ - // 服务中 - report.setUsers(getCache().getInservAgentUsersSizeByOrgi(orgi)); - // 等待中 - report.setInquene(getCache().getInqueAgentUsersSizeByOrgi(orgi)); - - // DEBUG -// logger.info( -// "[getAgentReport] orgi {}, organ {}, agents {}, busy {}, users {}, inqueue {}", orgi, organ, -// report.getAgents(), report.getBusy(), report.getUsers(), report.getInquene() -// ); - return report; - } - - @SuppressWarnings("unchecked") - public static int getQueueIndex(String agent, String orgi, String skill) { - int queneUsers = 0; - Map map = getCache().getAgentUsersInQueByOrgi(orgi); - - for (Map.Entry entry : map.entrySet()) { - if (StringUtils.isNotBlank(skill)) { - if (StringUtils.equals(entry.getValue().getSkill(), skill)) { - queneUsers++; - } - continue; - } else { - if (StringUtils.isNotBlank(agent)) { - if (StringUtils.equals(entry.getValue().getAgentno(), agent)) { - queneUsers++; - } - continue; - } else { - queneUsers++; - } - } - } - return queneUsers; - } - - /** - * 为坐席批量分配用户 - * - * @param agentno - * @param orgi - */ - @SuppressWarnings("unchecked") - public static void allotAgent(String agentno, String orgi) { - // 获得目标坐席的状态 - AgentStatus agentStatus = SerializeUtil.deserialize( - getRedisCommand().getHashKV(RedisKey.getAgentStatusReadyHashKey(orgi), agentno)); - - if (agentStatus == null) { - logger.warn("[allotAgent] can not find AgentStatus for agentno {}", agentno); - return; - } - - // 获得所有待服务访客的列表 - Map pendingAgentUsers = getCache().getAgentUsersInQueByOrgi(orgi); - - for (Map.Entry entry : pendingAgentUsers.entrySet()) { - AgentUser agentUser = entry.getValue(); - boolean process = false; - - if ((StringUtils.equals(agentUser.getAgentno(), agentno))) { - // 待服务的访客指定了该坐席 - process = true; - } else { - if (agentStatus != null && - agentStatus.getSkills() != null && - agentStatus.getSkills().size() > 0) { - // 目标坐席有状态,并且坐席属于某技能组 - if ((StringUtils.isBlank(agentUser.getAgentno()) && - StringUtils.isBlank(agentUser.getSkill()))) { - // 待服务的访客还没有指定坐席,并且也没有绑定技能组 - process = true; - } else { - if (StringUtils.isBlank(agentUser.getAgentno()) && - agentStatus.getSkills().containsKey(agentUser.getSkill())) { - // 待服务的访客还没有指定坐席,并且指定的技能组和该坐席的技能组一致 - process = true; - } - } - } else { - // 目标坐席没有状态,或该目标坐席有状态但是没有属于任何一个技能组 - if (StringUtils.isBlank(agentUser.getAgentno()) && - StringUtils.isBlank(agentUser.getSkill())) { - // 待服务访客没有指定坐席,并且没有指定技能组 - process = true; - } - } - } - - if (!process) { - continue; - } - - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); - long maxusers = sessionConfig == null ? Constants.AGENT_STATUS_MAX_USER : sessionConfig.getMaxuser(); - if (agentStatus.getUsers() < maxusers) { //坐席未达到最大咨询访客数量 - // 从排队队列移除 - getCache().deleteAgentUserInqueByAgentUserIdAndOrgi(agentUser.getUserid(), orgi); - - // 下面开始处理其加入到服务中的队列 - try { - AgentService agentService = processAgentService(agentStatus, agentUser, orgi, sessionConfig); - - // 处理完成得到 agentService - Message outMessage = new Message(); - outMessage.setMessage(AutomaticServiceDist.getSuccessMessage( - agentService, - agentUser.getChannel(), - orgi)); - outMessage.setMessageType(MainContext.MediaType.TEXT.toString()); - outMessage.setCalltype(MainContext.CallType.IN.toString()); - outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); - - if (StringUtils.isNotBlank(agentUser.getUserid())) { - outMessage.setAgentUser(agentUser); - outMessage.setChannelMessage(agentUser); - - // 向访客推送消息 - getPeerSyncIM().send( - MainContext.ReceiverType.VISITOR, - MainContext.ChannelType.toValue(agentUser.getChannel()), agentUser.getAppid(), - MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true - ); - - // 向坐席推送消息 - getPeerSyncIM().send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, - agentUser.getAppid(), - MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true); - } - } catch (Exception ex) { - logger.warn("[allotAgent] fail to process service", ex); - } - } else { - logger.info("[allotAgent] agentno {} reach the max users limit", agentno); - break; - } - } - broadcastAgentsStatus(orgi, "agent", "success", agentno); - } - - /** - * 访客服务结束 - * - * @param agentUser - * @param orgi - * @throws Exception - */ - public static void serviceFinish(final AgentUser agentUser, final String orgi) { - if (agentUser != null) { - // 获得坐席状态 - AgentStatus agentStatus = null; - if (StringUtils.equals(MainContext.AgentUserStatusEnum.INSERVICE.toString(), agentUser.getStatus()) && - agentUser.getAgentno() != null) { - agentStatus = getCache().findOneAgentStatusByAgentnoAndOrig(agentUser.getAgentno(), orgi); - } - - // 设置新AgentUser的状态 - agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - if (agentUser.getServicetime() != null) { - agentUser.setSessiontimes(System.currentTimeMillis() - agentUser.getServicetime().getTime()); - } - - // 从缓存中删除agentUser缓存 - getAgentUserRes().save(agentUser); - - final SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); - - // 坐席服务 - AgentService service = null; - if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - service = getAgentServiceRes().findByIdAndOrgi(agentUser.getAgentserviceid(), agentUser.getOrgi()); - } else if (agentStatus != null) { - // 该访客没有和坐席对话,因此没有 AgentService - // 当做留言处理,创建一个新的 AgentService - service = processAgentService(agentStatus, agentUser, orgi, true, sessionConfig); - } - - if (service != null) { - service.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - service.setEndtime(new Date()); - if (service.getServicetime() != null) { - service.setSessiontimes(System.currentTimeMillis() - service.getServicetime().getTime()); - } - - final List agentUserTaskList = getAgentUserTaskRes().findByIdAndOrgi( - agentUser.getId(), agentUser.getOrgi()); - if (agentUserTaskList.size() > 0) { - final AgentUserTask agentUserTask = agentUserTaskList.get(0); - service.setAgentreplyinterval(agentUserTask.getAgentreplyinterval()); - service.setAgentreplytime(agentUserTask.getAgentreplytime()); - service.setAvgreplyinterval(agentUserTask.getAvgreplyinterval()); - service.setAvgreplytime(agentUserTask.getAvgreplytime()); - - service.setUserasks(agentUserTask.getUserasks()); - service.setAgentreplys(agentUserTask.getAgentreplys()); - - // 开启了质检,并且是有效对话 - if (sessionConfig.isQuality()) { - // 未分配质检任务 - service.setQualitystatus(MainContext.QualityStatusEnum.NODIS.toString()); - } - } - - /** - * 启用了质检任务,开启质检 - */ - if ((!sessionConfig.isQuality()) || service.getUserasks() == 0) { - // 未开启质检 或无效对话无需质检 - service.setQualitystatus(MainContext.QualityStatusEnum.NO.toString()); - } - getAgentServiceRes().save(service); - } - - /** - * 发送到访客端的通知 - */ - switch (MainContext.ChannelType.toValue(agentUser.getChannel())) { - case WEBIM: - // WebIM 发送对话结束事件 - // 向访客发送消息 - Message outMessage = new Message(); - outMessage.setAgentStatus(agentStatus); - outMessage.setMessage(AutomaticServiceDist.getServiceFinishMessage(agentUser.getChannel(), orgi)); - outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString()); - outMessage.setCalltype(MainContext.CallType.IN.toString()); - outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); - outMessage.setAgentUser(agentUser); - - // 向访客发送消息 - getPeerSyncIM().send( - MainContext.ReceiverType.VISITOR, - MainContext.ChannelType.toValue(agentUser.getChannel()), agentUser.getAppid(), - MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true - ); - - if (agentStatus != null) { - // 坐席在线,通知结束会话 - outMessage.setChannelMessage(agentUser); - outMessage.setAgentUser(agentUser); - getPeerSyncIM().send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, - agentUser.getAppid(), - MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true); - } - break; - case PHONE: - // 语音渠道,强制发送 - logger.info("[serviceFinish] send notify to callout channel agentno {}", agentUser.getAgentno()); - NettyClients.getInstance().sendCalloutEventMessage( - agentUser.getAgentno(), MainContext.MessageType.END.toString(), agentUser); - break; - default: - logger.info( - "[serviceFinish] ignore notify agent service end for channel {}, agent user id {}", - agentUser.getChannel(), agentUser.getId()); - } - - // 更新访客的状态为可以接收邀请 - final OnlineUser onlineUser = getOnlineUserRes().findOneByUseridAndOrgi( - agentUser.getUserid(), agentUser.getOrgi()); - if (onlineUser != null) { - onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString()); - getOnlineUserRes().save(onlineUser); - logger.info( - "[online] onlineUser id {}, status {}, invite status {}", onlineUser.getId(), - onlineUser.getStatus(), onlineUser.getInvitestatus()); - } - - // 当前访客服务已经结束,为坐席寻找新访客 - if (agentStatus != null) { - long maxusers = sessionConfig != null ? sessionConfig.getMaxuser() : Constants.AGENT_STATUS_MAX_USER; - if ((agentStatus.getUsers() - 1) < maxusers) { - allotAgent(agentStatus.getAgentno(), orgi); - } - } - broadcastAgentsStatus(orgi, "end", "success", agentUser != null ? agentUser.getId() : null); - } else { - logger.info("[serviceFinish] orgi {}, invalid agent user, should not be null", orgi); - } - } - - /** - * 更新坐席当前服务中的用户状态 - * #TODO 需要分布式锁 - * - * @param agentStatus - * @param orgi - */ - public synchronized static void updateAgentStatus(AgentStatus agentStatus, String orgi) { - int users = getCache().getInservAgentUsersSizeByAgentnoAndOrgi(agentStatus.getAgentno(), orgi); - agentStatus.setUsers(users); - agentStatus.setUpdatetime(new Date()); - getCache().putAgentStatusByOrgi(agentStatus, orgi); - } - - /** - * 向所有client通知坐席状态变化 - * - * @param orgi - * @param worktype - * @param workresult - * @param dataid - */ - public static void broadcastAgentsStatus(final String orgi, final String worktype, final String workresult, final String dataid) { - /** - * 坐席状态改变,通知监测服务 - */ - AgentReport agentReport = AutomaticServiceDist.getAgentReport(orgi); - agentReport.setOrgi(orgi); - agentReport.setWorktype(worktype); - agentReport.setWorkresult(workresult); - agentReport.setDataid(dataid); - getAgentReportRes().save(agentReport); - MainContext.getContext().getBean("agentNamespace", SocketIONamespace.class).getBroadcastOperations().sendEvent( - "status", agentReport); - } - - - /** - * @param agent 坐席 - * @param userid 用户ID - * @param status 工作状态,也就是上一个状态 - * @param current 下一个工作状态 - * @param worktype 类型 : 语音OR 文本 - * @param orgi - * @param lasttime - */ - public static void recordAgentStatus( - String agent, - String username, - String extno, - boolean admin, - String userid, - String status, - String current, - String worktype, - String orgi, - Date lasttime - ) { - WorkMonitorRepository workMonitorRes = MainContext.getContext().getBean(WorkMonitorRepository.class); - WorkMonitor workMonitor = new WorkMonitor(); - if (StringUtils.isNotBlank(agent) && StringUtils.isNotBlank(status)) { - workMonitor.setAgent(agent); - workMonitor.setAgentno(agent); - workMonitor.setStatus(status); - workMonitor.setAdmin(admin); - workMonitor.setUsername(username); - workMonitor.setExtno(extno); - workMonitor.setWorktype(worktype); - if (lasttime != null) { - workMonitor.setDuration((int) (System.currentTimeMillis() - lasttime.getTime()) / 1000); - } - if (status.equals(MainContext.AgentStatusEnum.BUSY.toString())) { - workMonitor.setBusy(true); - } - if (status.equals(MainContext.AgentStatusEnum.READY.toString())) { - int count = workMonitorRes.countByAgentAndDatestrAndStatusAndOrgi( - agent, MainUtils.simpleDateFormat.format(new Date()), - MainContext.AgentStatusEnum.READY.toString(), orgi - ); - if (count == 0) { - workMonitor.setFirsttime(true); - } - } - if (current.equals(MainContext.AgentStatusEnum.NOTREADY.toString())) { - List workMonitorList = workMonitorRes.findByOrgiAndAgentAndDatestrAndFirsttime( - orgi, agent, MainUtils.simpleDateFormat.format(new Date()), true); - if (workMonitorList.size() > 0) { - WorkMonitor firstWorkMonitor = workMonitorList.get(0); - if (firstWorkMonitor.getFirsttimes() == 0) { - firstWorkMonitor.setFirsttimes( - (int) (System.currentTimeMillis() - firstWorkMonitor.getCreatetime().getTime())); - workMonitorRes.save(firstWorkMonitor); - } - } - } - workMonitor.setCreatetime(new Date()); - workMonitor.setDatestr(MainUtils.simpleDateFormat.format(new Date())); - - workMonitor.setName(agent); - workMonitor.setOrgi(orgi); - workMonitor.setUserid(userid); - - workMonitorRes.save(workMonitor); - } - } - - /** - * 撤退一个坐席 - * 1)将该坐席状态置为"非就绪" - * 2) 将该坐席的访客重新分配给其它坐席 - * - * @param orgi - * @param agentno - * @return 有没有成功将所有其服务的访客都分配出去 - */ - public static boolean withdrawAgent(final String orgi, final String agentno) { - // 先将该客服切换到非就绪状态 - final AgentStatus agentStatus = getCache().findOneAgentStatusByAgentnoAndOrig(agentno, orgi); - if (agentStatus != null) { - agentStatus.setBusy(false); - agentStatus.setUpdatetime(new Date()); - agentStatus.setStatus(MainContext.AgentStatusEnum.NOTREADY.toString()); - getAgentStatusRes().save(agentStatus); - cache.putAgentStatusByOrgi(agentStatus, orgi); - } - - // 然后将该坐席的访客分配给其它坐席 - // 获得该租户在线的客服的多少 - // TODO 对于agentUser的技能组过滤,在下面再逐个考虑? - // 该信息同样也包括当前用户 - List agentUsers = cache.findInservAgentUsersByAgentnoAndOrgi(agentno, orgi); - int sz = agentUsers.size(); - for (final AgentUser x : agentUsers) { - try { - // TODO 此处没有考虑遍历过程中,系统中坐席的服务访客的信息实际上是变化的 - // 可能会发生maxusers超过设置的情况,如果做很多检查,会带来一定一系统开销 - // 因为影响不大,放弃实时的检查 - allotAgent(x, x.getOrgi()); - // 因为重新分配该访客,将其从撤离的坐席中服务集合中删除 - // 此处类似于 Transfer - getRedisCommand().removeSetVal( - RedisKey.getInServAgentUsersByAgentnoAndOrgi(agentno, orgi), x.getUserid()); - sz--; - } catch (Exception e) { - logger.warn("[withdrawAgent] throw error:", e); - } - } - - if (sz == 0) { - logger.info("[withdrawAgent] after re-allotAgent, the agentUsers size is {} for agentno {}", sz, agentno); - } else { - logger.warn("[withdrawAgent] after re-allotAgent, the agentUsers size is {} for agentno {}", sz, agentno); - } - - return sz == 0; - } - - /** - * 为用户分配坐席 - * - * @param agentUser - */ - @SuppressWarnings("unchecked") - public static AgentService allotAgent( - final AgentUser agentUser, - final String orgi) { - /** - * 查询条件,当前在线的 坐席,并且 未达到最大 服务人数的坐席 - */ - - List agentStatusList = filterOutAvailableAgentStatus(agentUser, orgi); - - /** - * 处理ACD 的 技能组请求和 坐席请求 - */ - AgentStatus agentStatus = null; - AgentService agentService = null; //放入缓存的对象 - SessionConfig sessionConfig = initSessionConfig(orgi); - if (agentStatusList.size() > 0) { - agentStatus = agentStatusList.get(0); - if (agentStatus.getUsers() >= sessionConfig.getMaxuser()) { - agentStatus = null; - /** - * 判断当前有多少人排队中 , 分三种情况:1、请求技能组的,2、请求坐席的,3,默认请求的 - * - */ - } - } - - try { - agentService = processAgentService(agentStatus, agentUser, orgi, sessionConfig); - // 处理结果:进入排队队列 - if (StringUtils.equals(MainContext.AgentUserStatusEnum.INQUENE.toString(), agentService.getStatus())) { - agentService.setQueneindex(getQueueIndex(agentUser.getAgentno(), orgi, agentUser.getSkill())); - } - } catch (Exception ex) { - logger.warn("[allotAgent] exception: ", ex); - } - broadcastAgentsStatus( - orgi, "user", agentService != null && agentService.getStatus().equals( - MainContext.AgentUserStatusEnum.INSERVICE.toString()) ? "inservice" : "inquene", - agentUser.getId() - ); - return agentService; - } - - /** - * 过滤在线客服 - * 优先级: 1. 指定坐席;2. 指定技能组; 3. 租户所有的坐席 - * - * @param agentUser - * @param orgi - * @return - */ - private static List filterOutAvailableAgentStatus( - final AgentUser agentUser, - final String orgi - ) { - logger.info( - "[filterOutAvailableAgentStatus] agentUser {}, orgi {}, skill {}, onlineUser {}", - agentUser.getAgentno(), orgi, agentUser.getSkill(), agentUser.getUserid() - ); - List agentStatuses = new ArrayList<>(); - Map map = getCache().findAllReadyAgentStatusByOrgi(orgi); - - if (agentUser != null && StringUtils.isNotBlank(agentUser.getAgentno())) { - // 指定坐席 - for (Map.Entry entry : map.entrySet()) { - if ((!entry.getValue().isBusy()) && (StringUtils.equals( - entry.getValue().getAgentno(), agentUser.getAgentno()))) { - agentStatuses.add(entry.getValue()); - } - } - } - - /** - * 指定坐席未查询到就绪的 - */ - if (agentStatuses.size() == 0) { - if (StringUtils.isNotBlank(agentUser.getSkill())) { - // 指定技能组 - for (Map.Entry entry : map.entrySet()) { - if ((!entry.getValue().isBusy()) && - (entry.getValue().getSkills() != null && - entry.getValue().getSkills().containsKey(agentUser.getSkill()))) { - agentStatuses.add(entry.getValue()); - } - } - } - } - - /** - * 在指定的坐席和技能组中未查到坐席 - * 接下来进行无差别查询 - */ - if (agentStatuses.size() == 0) { - // 对于该租户的所有客服 - for (Map.Entry entry : map.entrySet()) { - if (!entry.getValue().isBusy()) { - agentStatuses.add(entry.getValue()); - } - } - } - - logger.info("[filterOutAvailableAgentStatus] agent status list size: {}", agentStatuses.size()); - return agentStatuses; - } - - /** - * 邀请访客进入当前对话,如果当前操作的 坐席是已就绪状态,则直接加入到当前坐席的 对话列表中,如果未登录,则分配给其他坐席 - * - * @param agentno - * @param agentUser - * @param orgi - * @return - * @throws Exception - */ - public static AgentService allotAgentForInvite( - final String agentno, - final AgentUser agentUser, - final String orgi - ) throws Exception { - AgentStatus agentStatus = getCache().findOneAgentStatusByAgentnoAndOrig(agentno, orgi); - AgentService agentService; - if (agentStatus != null) { - SessionConfig sessionConfig = initSessionConfig(orgi); - agentService = processAgentService(agentStatus, agentUser, orgi, sessionConfig); - broadcastAgentsStatus(orgi, "invite", "success", agentno); - - /** - * 通知坐席新的访客邀请成功 - */ - Message outMessage = new Message(); - outMessage.setAgentUser(agentUser); - outMessage.setChannelMessage(agentUser); - getPeerSyncIM().send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, - agentUser.getAppid(), - MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true); - } else { - agentService = allotAgent(agentUser, orgi); - } - return agentService; - } - - /** - * 为访客 分配坐席, ACD策略,此处 AgentStatus 是建议 的 坐席, 如果启用了 历史服务坐席 优先策略, 则会默认检查历史坐席是否空闲,如果空闲,则分配,如果不空闲,则 分配当前建议的坐席 - * - * @param agentStatus - * @param agentUser - * @param orgi - * @return - * @throws Exception - */ - private static AgentService processAgentService(AgentStatus agentStatus, final AgentUser agentUser, final String orgi, final SessionConfig sessionConfig) throws Exception { - return processAgentService(agentStatus, agentUser, orgi, false, sessionConfig); - } - - /** - * 为访客分配机器人客服, ACD策略,此处 AgentStatus 是建议 的 坐席, 如果启用了 历史服务坐席 优先策略, 则会默认检查历史坐席是否空闲,如果空闲,则分配,如果不空闲,则 分配当前建议的坐席 - * - * @param agentUser - * @param orgi - * @return - * @throws Exception - */ - public static AgentService processChatbotService(final String botName, final AgentUser agentUser, final String orgi) { - AgentService agentService = new AgentService(); //放入缓存的对象 - Date now = new Date(); - if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - agentService = getAgentServiceRes().findByIdAndOrgi(agentUser.getAgentserviceid(), orgi); - agentService.setEndtime(now); - if (agentService.getServicetime() != null) { - agentService.setSessiontimes(System.currentTimeMillis() - agentService.getServicetime().getTime()); - } - agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - } else { - agentService.setServicetime(now); - agentService.setLogindate(now); - agentService.setOrgi(orgi); - agentService.setOwner(agentUser.getContextid()); - agentService.setSessionid(agentUser.getSessionid()); - agentService.setRegion(agentUser.getRegion()); - agentService.setUsername(agentUser.getUsername()); - agentService.setChannel(agentUser.getChannel()); - if (botName != null) { - agentService.setAgentusername(botName); - } - - if (StringUtils.isNotBlank(agentUser.getContextid())) { - agentService.setContextid(agentUser.getContextid()); - } else { - agentService.setContextid(agentUser.getSessionid()); - } - - agentService.setUserid(agentUser.getUserid()); - agentService.setAiid(agentUser.getAgentno()); - agentService.setAiservice(true); - agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); - - agentService.setAppid(agentUser.getAppid()); - agentService.setLeavemsg(false); - } - - agentServiceRes.save(agentService); - return agentService; - } - - /** - * 为agentUser生成对应的AgentService - * 使用场景: - * 1. 在AgentUser服务结束并且还没有对应的AgentService - * 2. 在新服务开始,安排坐席 - * - * @param agentStatus 坐席状态 - * @param agentUser 坐席访客会话 - * @param orgi 租户ID - * @param finished 结束服务 - * @param sessionConfig 坐席配置 - * @return - */ - private static AgentService processAgentService( - AgentStatus agentStatus, - final AgentUser agentUser, - final String orgi, - final boolean finished, - final SessionConfig sessionConfig) { - AgentService agentService = new AgentService(); - if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - agentService.setId(agentUser.getAgentserviceid()); - } - agentService.setOrgi(orgi); - - final Date now = new Date(); - // 批量复制属性 - MainUtils.copyProperties(agentUser, agentService); - agentService.setChannel(agentUser.getChannel()); - agentService.setSessionid(agentUser.getSessionid()); - - // 此处为何设置loginDate为现在 - agentUser.setLogindate(now); - OnlineUser onlineUser = getOnlineUserRes().findOneByUseridAndOrgi(agentUser.getUserid(), orgi); - - if (finished == true) { - // 服务结束 - agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - agentService.setSessiontype(MainContext.AgentUserStatusEnum.END.toString()); - if (agentStatus == null) { - // 没有满足条件的坐席,留言 - agentService.setLeavemsg(true); - agentService.setLeavemsgstatus(MainContext.LeaveMsgStatus.NOTPROCESS.toString()); //未处理的留言 - } - - if (onlineUser != null) { - // 更新OnlineUser对象,变更为默认状态,可以接受邀请 - onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString()); - } - } else if (agentStatus != null) { - agentService.setAgent(agentStatus.getAgentno()); - agentService.setSkill(agentUser.getSkill()); - - if (sessionConfig.isLastagent()) { - // 启用了历史坐席优先 , 查找 历史服务坐席 - List webIMaggList = MainUtils.getWebIMDataAgg( - onlineUserRes.findByOrgiForDistinctAgent(orgi, agentUser.getUserid())); - if (webIMaggList.size() > 0) { - for (WebIMReport report : webIMaggList) { - if (report.getData().equals(agentStatus.getAgentno())) { - break; - } else { - AgentStatus hisAgentStatus = getCache().findOneAgentStatusByAgentnoAndOrig( - report.getData(), orgi); - if (hisAgentStatus != null && hisAgentStatus.getUsers() < hisAgentStatus.getMaxusers()) { - // 变更为 历史服务坐席 - agentStatus = hisAgentStatus; - break; - } - } - - } - } - } - - agentUser.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); - agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); - agentService.setSessiontype(MainContext.AgentUserStatusEnum.INSERVICE.toString()); - - // 设置坐席名字 - agentService.setAgentno(agentStatus.getUserid()); - agentService.setAgentusername(agentStatus.getUsername()); - } else { - // 不是服务结束,但是没有满足条件的坐席 - // 加入到排队中 - agentUser.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString()); - agentService.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString()); - agentService.setSessiontype(MainContext.AgentUserStatusEnum.INQUENE.toString()); - } - - if (finished || agentStatus != null) { - agentService.setAgentuserid(agentUser.getId()); - agentService.setInitiator(MainContext.ChatInitiatorType.USER.toString()); - - long waittingtime = 0; - if (agentUser.getWaittingtimestart() != null) { - waittingtime = System.currentTimeMillis() - agentUser.getWaittingtimestart().getTime(); - } else { - if (agentUser.getCreatetime() != null) { - waittingtime = System.currentTimeMillis() - agentUser.getCreatetime().getTime(); - } - } - - agentUser.setWaittingtime((int) waittingtime); - agentUser.setServicetime(now); - agentService.setOwner(agentUser.getOwner()); - agentService.setTimes(0); - - final User agent = getUserRes().findOne(agentService.getAgentno()); - agentUser.setAgentname(agent.getUname()); - agentUser.setAgentno(agentService.getAgentno()); - - if (StringUtils.isNotBlank(agentUser.getName())) { - agentService.setName(agentUser.getName()); - } - if (StringUtils.isNotBlank(agentUser.getPhone())) { - agentService.setPhone(agentUser.getPhone()); - } - if (StringUtils.isNotBlank(agentUser.getEmail())) { - agentService.setEmail(agentUser.getEmail()); - } - if (StringUtils.isNotBlank(agentUser.getResion())) { - agentService.setResion(agentUser.getResion()); - } - - if (StringUtils.isNotBlank(agentUser.getSkill())) { - agentService.setAgentskill(agentUser.getSkill()); - } - - agentService.setServicetime(now); - - if (agentUser.getCreatetime() != null) { - agentService.setWaittingtime((int) (System.currentTimeMillis() - agentUser.getCreatetime().getTime())); - agentUser.setWaittingtime(agentService.getWaittingtime()); - } - if (onlineUser != null) { - agentService.setOsname(onlineUser.getOpersystem()); - agentService.setBrowser(onlineUser.getBrowser()); - // 记录onlineUser的id - agentService.setDataid(onlineUser.getId()); - } - - agentService.setLogindate(agentUser.getCreatetime()); - getAgentServiceRes().save(agentService); - - agentUser.setAgentserviceid(agentService.getId()); - agentUser.setLastgetmessage(now); - agentUser.setLastmessage(now); - } - - agentService.setDataid(agentUser.getId()); - - /** - * 分配成功以后, 将用户和坐席的对应关系放入到缓存 - * 将 AgentUser 放入到当前坐席的服务队列 - */ - getAgentUserRes().save(agentUser); - - /** - * 更新OnlineUser对象,变更为服务中,不可邀请 - */ - if (onlineUser != null && !finished) { - onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.INSERV.toString()); - onlineUserRes.save(onlineUser); - } - - // 更新坐席服务人数,坐席更新时间到缓存 - if (agentStatus != null) { - updateAgentStatus(agentStatus, orgi); - } - return agentService; - } - - /** - * 删除AgentUser - * 包括数据库记录及缓存信息 - * - * @param agentUser - * @param orgi - * @return - */ - public static void deleteAgentUser(final AgentUser agentUser, final String orgi) throws CSKefuException { - logger.info("[deleteAgentUser] userId {}, orgi {}", agentUser.getUserid(), orgi); - - if (agentUser == null || agentUser.getId() == null) { - throw new CSKefuException("Invalid agentUser info"); - } - - if (!StringUtils.equals(MainContext.AgentUserStatusEnum.END.toString(), agentUser.getStatus())) { - /** - * 未结束聊天,先结束对话,然后删除记录 - */ - // 删除缓存 - serviceFinish(agentUser, orgi); - } - - // 删除数据库里的AgentUser记录 - getAgentUserRes().delete(agentUser); - } - - /** - * 通知消息内容:分配到坐席 - * - * @param agentService - * @param channel - * @param orgi - * @return - */ - public static String getSuccessMessage(AgentService agentService, String channel, String orgi) { - String queneTip = "" + agentService.getAgentusername() + ""; - if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { - queneTip = agentService.getAgentusername(); - } - SessionConfig sessionConfig = initSessionConfig(orgi); - String successMsg = "坐席分配成功," + queneTip + "为您服务。"; - if (StringUtils.isNotBlank(sessionConfig.getSuccessmsg())) { - successMsg = sessionConfig.getSuccessmsg().replaceAll("\\{agent\\}", queneTip); - } - return successMsg; - } - - /** - * 通知消息内容:和坐席断开 - * - * @param channel - * @param orgi - * @return - */ - public static String getServiceFinishMessage(String channel, String orgi) { - SessionConfig sessionConfig = initSessionConfig(orgi); - String queneTip = "坐席已断开和您的对话"; - if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) { - queneTip = sessionConfig.getFinessmsg(); - } - return queneTip; - } - - - /** - * 通知消息内容:和坐席断开,刷新页面 - * - * @param channel - * @param orgi - * @return - */ - public static String getServiceOffMessage(String channel, String orgi) { - SessionConfig sessionConfig = initSessionConfig(orgi); - String queneTip = "坐席已断开和您的对话,刷新页面为您分配新的坐席"; - if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) { - queneTip = sessionConfig.getFinessmsg(); - } - return queneTip; - } - - public static String getNoAgentMessage(int queneIndex, String channel, String orgi) { - if (queneIndex < 0) { - queneIndex = 0; - } - String queneTip = "" + queneIndex + ""; - if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { - queneTip = String.valueOf(queneIndex); - } - SessionConfig sessionConfig = initSessionConfig(orgi); - String noAgentTipMsg = "坐席全忙,已进入等待队列,您也可以在其他时间再来咨询。"; - if (StringUtils.isNotBlank(sessionConfig.getNoagentmsg())) { - noAgentTipMsg = sessionConfig.getNoagentmsg().replaceAll("\\{num\\}", queneTip); - } - return noAgentTipMsg; - } - - public static String getQueneMessage(int queneIndex, String channel, String orgi) { - - String queneTip = "" + queneIndex + ""; - if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) { - queneTip = String.valueOf(queneIndex); - } - SessionConfig sessionConfig = initSessionConfig(orgi); - String agentBusyTipMsg = "正在排队,请稍候,在您之前,还有 " + queneTip + " 位等待用户。"; - if (StringUtils.isNotBlank(sessionConfig.getAgentbusymsg())) { - agentBusyTipMsg = sessionConfig.getAgentbusymsg().replaceAll("\\{num\\}", queneTip); - } - return agentBusyTipMsg; - } - - private static RedisCommand getRedisCommand() { - if (redisCommand == null) { - redisCommand = MainContext.getContext().getBean(RedisCommand.class); - } - return redisCommand; - } - - private static Cache getCache() { - if (cache == null) { - cache = MainContext.getContext().getBean(Cache.class); - } - return cache; - } - - private static AgentUserRepository getAgentUserRes() { - if (agentUserRes == null) { - agentUserRes = MainContext.getContext().getBean(AgentUserRepository.class); - } - return agentUserRes; - } - - private static OnlineUserRepository getOnlineUserRes() { - if (onlineUserRes == null) { - onlineUserRes = MainContext.getContext().getBean(OnlineUserRepository.class); - } - return onlineUserRes; - } - - private static AgentServiceRepository getAgentServiceRes() { - if (agentServiceRes == null) { - agentServiceRes = MainContext.getContext().getBean(AgentServiceRepository.class); - } - return agentServiceRes; - } - - private static AgentUserTaskRepository getAgentUserTaskRes() { - if (agentUserTaskRes == null) { - agentUserTaskRes = MainContext.getContext().getBean(AgentUserTaskRepository.class); - } - return agentUserTaskRes; - } - - private static AgentReportRepository getAgentReportRes() { - if (agentReportRes == null) { - agentReportRes = MainContext.getContext().getBean(AgentReportRepository.class); - } - return agentReportRes; - } - - private static AgentStatusRepository getAgentStatusRes() { - if (agentStatusRes == null) { - agentStatusRes = MainContext.getContext().getBean(AgentStatusRepository.class); - } - - return agentStatusRes; - } - - private static SessionConfigRepository getSessionConfigRes() { - if (sessionConfigRes == null) { - sessionConfigRes = MainContext.getContext().getBean(SessionConfigRepository.class); - } - - return sessionConfigRes; - } - - private static UserRepository getUserRes() { - if (UserRes == null) { - UserRes = MainContext.getContext().getBean(UserRepository.class); - } - - return UserRes; - } - - private static ContactsRepository getContactsRes() { - if (contactsRes == null) { - contactsRes = MainContext.getContext().getBean(ContactsRepository.class); - } - - return contactsRes; - } - - private static AgentUserContactsRepository getAgentUserContactsRes() { - if (agentUserContactsRes == null) { - agentUserContactsRes = MainContext.getContext().getBean(AgentUserContactsRepository.class); - } - - return agentUserContactsRes; - } - - private static AgentAuditProxy getAgentAuditProxy() { - if (agentAuditProxy == null) { - agentAuditProxy = MainContext.getContext().getBean(AgentAuditProxy.class); - } - return agentAuditProxy; - } - - private static PeerSyncIM getPeerSyncIM() { - if (peerSyncIM == null) { - peerSyncIM = MainContext.getContext().getBean(PeerSyncIM.class); - } - return peerSyncIM; - } - - -} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/agent/ACDAgentMw1.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/agent/ACDAgentMw1.java new file mode 100644 index 00000000..919fdc9d --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/agent/ACDAgentMw1.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.agent; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * 为坐席分配访客 + */ +@Component +public class ACDAgentMw1 implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(ACDAgentMw1.class); + + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisAllocatorMw.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisAllocatorMw.java new file mode 100644 index 00000000..718d0d7f --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisAllocatorMw.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.visitor; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.cc.acd.ACDQueueService; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.*; +import com.chatopera.cc.persistence.repository.AgentServiceRepository; +import com.chatopera.cc.persistence.repository.AgentUserRepository; +import com.chatopera.cc.persistence.repository.OnlineUserRepository; +import com.chatopera.cc.persistence.repository.UserRepository; +import com.chatopera.cc.proxy.AgentUserProxy; +import com.chatopera.cc.util.WebIMReport; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Component +public class ACDVisAllocatorMw implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(ACDVisAllocatorMw.class); + + @Autowired + private Cache cache; + + @Autowired + private ACDQueueService acdQueueService; + + @Autowired + private AgentServiceRepository agentServiceRes; + + @Autowired + private OnlineUserRepository onlineUserRes; + + @Autowired + private UserRepository userRes; + + @Autowired + private AgentUserRepository agentUserRes; + + @Autowired + private AgentUserProxy agentUserProxy; + + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + + /** + * 查询条件,当前在线的 坐席,并且 未达到最大 服务人数的坐席 + */ + List agentStatuses = filterOutAvailableAgentStatus( + ctx.getAgentUser(), ctx.getOrgi()); + /** + * 处理ACD 的 技能组请求和 坐席请求 + */ + AgentStatus agentStatus = null; + AgentService agentService = null; //放入缓存的对象 + + if (agentStatuses.size() > 0) { + agentStatus = agentStatuses.get(0); + if (agentStatus.getUsers() >= ctx.getSessionConfig().getMaxuser()) { + agentStatus = null; + /** + * 判断当前有多少人排队中 , 分三种情况:1、请求技能组的,2、请求坐席的,3,默认请求的 + * + */ + } + } + + try { + agentService = processAgentService( + agentStatus, ctx.getAgentUser(), ctx.getOrgi(), false, ctx.getSessionConfig()); + // 处理结果:进入排队队列 + if (StringUtils.equals(MainContext.AgentUserStatusEnum.INQUENE.toString(), agentService.getStatus())) { + agentService.setQueneindex( + acdQueueService.getQueueIndex( + ctx.getAgentUser().getAgentno(), ctx.getOrgi(), ctx.getAgentUser().getSkill())); + } + } catch (Exception ex) { + logger.warn("[allotAgent] exception: ", ex); + } + + agentUserProxy.broadcastAgentsStatus( + ctx.getOrgi(), "user", agentService != null && agentService.getStatus().equals( + MainContext.AgentUserStatusEnum.INSERVICE.toString()) ? "inservice" : "inquene", + ctx.getAgentUser().getId()); + ctx.setAgentService(agentService); + + } + + /** + * 过滤在线客服 + * 优先级: 1. 指定坐席;2. 指定技能组; 3. 租户所有的坐席 + * + * @param agentUser + * @param orgi + * @return + */ + public List filterOutAvailableAgentStatus( + final AgentUser agentUser, + final String orgi + ) { + logger.info( + "[filterOutAvailableAgentStatus] agentUser {}, orgi {}, skill {}, onlineUser {}", + agentUser.getAgentno(), orgi, agentUser.getSkill(), agentUser.getUserid() + ); + List agentStatuses = new ArrayList<>(); + Map map = cache.findAllReadyAgentStatusByOrgi(orgi); + + if (agentUser != null && StringUtils.isNotBlank(agentUser.getAgentno())) { + // 指定坐席 + for (Map.Entry entry : map.entrySet()) { + if ((!entry.getValue().isBusy()) && (StringUtils.equals( + entry.getValue().getAgentno(), agentUser.getAgentno()))) { + agentStatuses.add(entry.getValue()); + } + } + } + + /** + * 指定坐席未查询到就绪的 + */ + if (agentStatuses.size() == 0) { + if (StringUtils.isNotBlank(agentUser.getSkill())) { + // 指定技能组 + for (Map.Entry entry : map.entrySet()) { + if ((!entry.getValue().isBusy()) && + (entry.getValue().getSkills() != null && + entry.getValue().getSkills().containsKey(agentUser.getSkill()))) { + agentStatuses.add(entry.getValue()); + } + } + } + } + + /** + * 在指定的坐席和技能组中未查到坐席 + * 接下来进行无差别查询 + */ + if (agentStatuses.size() == 0) { + // 对于该租户的所有客服 + for (Map.Entry entry : map.entrySet()) { + if (!entry.getValue().isBusy()) { + agentStatuses.add(entry.getValue()); + } + } + } + + logger.info("[filterOutAvailableAgentStatus] agent status list size: {}", agentStatuses.size()); + return agentStatuses; + } + + + /** + * 为agentUser生成对应的AgentService + * 使用场景: + * 1. 在AgentUser服务结束并且还没有对应的AgentService + * 2. 在新服务开始,安排坐席 + * + * @param agentStatus 坐席状态 + * @param agentUser 坐席访客会话 + * @param orgi 租户ID + * @param finished 结束服务 + * @param sessionConfig 坐席配置 + * @return + */ + public AgentService processAgentService( + AgentStatus agentStatus, + final AgentUser agentUser, + final String orgi, + final boolean finished, + final SessionConfig sessionConfig) { + AgentService agentService = new AgentService(); + if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + agentService.setId(agentUser.getAgentserviceid()); + } + agentService.setOrgi(orgi); + + final Date now = new Date(); + // 批量复制属性 + MainUtils.copyProperties(agentUser, agentService); + agentService.setChannel(agentUser.getChannel()); + agentService.setSessionid(agentUser.getSessionid()); + + // 此处为何设置loginDate为现在 + agentUser.setLogindate(now); + OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(agentUser.getUserid(), orgi); + + if (finished == true) { + // 服务结束 + agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + agentService.setSessiontype(MainContext.AgentUserStatusEnum.END.toString()); + if (agentStatus == null) { + // 没有满足条件的坐席,留言 + agentService.setLeavemsg(true); + agentService.setLeavemsgstatus(MainContext.LeaveMsgStatus.NOTPROCESS.toString()); //未处理的留言 + } + + if (onlineUser != null) { + // 更新OnlineUser对象,变更为默认状态,可以接受邀请 + onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString()); + } + } else if (agentStatus != null) { + agentService.setAgent(agentStatus.getAgentno()); + agentService.setSkill(agentUser.getSkill()); + + if (sessionConfig.isLastagent()) { + // 启用了历史坐席优先 , 查找 历史服务坐席 + List webIMaggList = MainUtils.getWebIMDataAgg( + onlineUserRes.findByOrgiForDistinctAgent(orgi, agentUser.getUserid())); + if (webIMaggList.size() > 0) { + for (WebIMReport report : webIMaggList) { + if (report.getData().equals(agentStatus.getAgentno())) { + break; + } else { + AgentStatus hisAgentStatus = cache.findOneAgentStatusByAgentnoAndOrig( + report.getData(), orgi); + if (hisAgentStatus != null && hisAgentStatus.getUsers() < hisAgentStatus.getMaxusers()) { + // 变更为 历史服务坐席 + agentStatus = hisAgentStatus; + break; + } + } + + } + } + } + + agentUser.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); + agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); + agentService.setSessiontype(MainContext.AgentUserStatusEnum.INSERVICE.toString()); + + // 设置坐席名字 + agentService.setAgentno(agentStatus.getUserid()); + agentService.setAgentusername(agentStatus.getUsername()); + } else { + // 不是服务结束,但是没有满足条件的坐席 + // 加入到排队中 + agentUser.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString()); + agentService.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString()); + agentService.setSessiontype(MainContext.AgentUserStatusEnum.INQUENE.toString()); + } + + if (finished || agentStatus != null) { + agentService.setAgentuserid(agentUser.getId()); + agentService.setInitiator(MainContext.ChatInitiatorType.USER.toString()); + + long waittingtime = 0; + if (agentUser.getWaittingtimestart() != null) { + waittingtime = System.currentTimeMillis() - agentUser.getWaittingtimestart().getTime(); + } else { + if (agentUser.getCreatetime() != null) { + waittingtime = System.currentTimeMillis() - agentUser.getCreatetime().getTime(); + } + } + + agentUser.setWaittingtime((int) waittingtime); + agentUser.setServicetime(now); + agentService.setOwner(agentUser.getOwner()); + agentService.setTimes(0); + + final User agent = userRes.findOne(agentService.getAgentno()); + agentUser.setAgentname(agent.getUname()); + agentUser.setAgentno(agentService.getAgentno()); + + if (StringUtils.isNotBlank(agentUser.getName())) { + agentService.setName(agentUser.getName()); + } + if (StringUtils.isNotBlank(agentUser.getPhone())) { + agentService.setPhone(agentUser.getPhone()); + } + if (StringUtils.isNotBlank(agentUser.getEmail())) { + agentService.setEmail(agentUser.getEmail()); + } + if (StringUtils.isNotBlank(agentUser.getResion())) { + agentService.setResion(agentUser.getResion()); + } + + if (StringUtils.isNotBlank(agentUser.getSkill())) { + agentService.setAgentskill(agentUser.getSkill()); + } + + agentService.setServicetime(now); + + if (agentUser.getCreatetime() != null) { + agentService.setWaittingtime((int) (System.currentTimeMillis() - agentUser.getCreatetime().getTime())); + agentUser.setWaittingtime(agentService.getWaittingtime()); + } + if (onlineUser != null) { + agentService.setOsname(onlineUser.getOpersystem()); + agentService.setBrowser(onlineUser.getBrowser()); + // 记录onlineUser的id + agentService.setDataid(onlineUser.getId()); + } + + agentService.setLogindate(agentUser.getCreatetime()); + agentServiceRes.save(agentService); + + agentUser.setAgentserviceid(agentService.getId()); + agentUser.setLastgetmessage(now); + agentUser.setLastmessage(now); + } + + agentService.setDataid(agentUser.getId()); + + /** + * 分配成功以后, 将用户和坐席的对应关系放入到缓存 + * 将 AgentUser 放入到当前坐席的服务队列 + */ + agentUserRes.save(agentUser); + + /** + * 更新OnlineUser对象,变更为服务中,不可邀请 + */ + if (onlineUser != null && !finished) { + onlineUser.setInvitestatus(MainContext.OnlineUserInviteStatus.INSERV.toString()); + onlineUserRes.save(onlineUser); + } + + // 更新坐席服务人数,坐席更新时间到缓存 + if (agentStatus != null) { + agentUserProxy.updateAgentStatus(agentStatus, orgi); + } + return agentService; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBindingMw.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBindingMw.java new file mode 100644 index 00000000..e324245c --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBindingMw.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.visitor; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.Organ; +import com.chatopera.cc.model.User; +import com.chatopera.cc.persistence.repository.OrganRepository; +import com.chatopera.cc.persistence.repository.UserRepository; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ACDVisBindingMw implements Middleware { + + private final static Logger logger = LoggerFactory.getLogger(ACDVisBindingMw.class); + + @Autowired + private UserRepository userRes; + + @Autowired + private OrganRepository organRes; + + @Autowired + private Cache cache; + + /** + * 绑定技能组或坐席 + * + * @param ctx + * @param next + */ + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + /** + * 访客新上线的请求 + */ + /** + * 技能组 和 坐席 + */ + if (StringUtils.isNotBlank(ctx.getOrganid())) { + logger.info("[apply] bind skill {}", ctx.getOrganid()); + // 绑定技能组 + Organ organ = organRes.findOne(ctx.getOrganid()); + if (organ != null) { + ctx.getAgentUser().setSkill(organ.getId()); + ctx.setOrgan(organ); + } + } else { + // 如果没有绑定技能组,则清除之前的标记 + ctx.getAgentUser().setSkill(null); + } + + if (StringUtils.isNotBlank(ctx.getAgentno())) { + logger.info("[apply] bind agentno {}", ctx.getAgentno()); + // 绑定坐席 + // 绑定坐席有可能是因为前端展示了技能组和坐席 + // 也有可能是坐席发送了邀请,该访客接收邀请 + ctx.getAgentUser().setAgentno(ctx.getAgentno()); + User agent = userRes.findOne(ctx.getAgentno()); + ctx.setAgent(agent); + ctx.getAgentUser().setAgentname(agent.getUname()); + } else { + // 如果没有绑定坐席,则清除之前的标记 + ctx.getAgentUser().setAgentno(null); + ctx.getAgentUser().setAgentname(null); + ctx.setAgent(null); + } + + next.apply(); + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBodyParserMw.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBodyParserMw.java new file mode 100644 index 00000000..66db451f --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisBodyParserMw.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.visitor; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.model.AgentUser; +import com.chatopera.cc.model.AgentUserContacts; +import com.chatopera.cc.model.Contacts; +import com.chatopera.cc.persistence.es.ContactsRepository; +import com.chatopera.cc.persistence.repository.AgentUserContactsRepository; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Resolve AgentUser + */ +@Component +public class ACDVisBodyParserMw implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(ACDVisBodyParserMw.class); + + @Autowired + private AgentUserContactsRepository agentUserContactsRes; + + @Autowired + private ContactsRepository contactsRes; + + @Autowired + private Cache cache; + + /** + * 设置AgentUser基本信息 + * + * @param ctx + * @param next + */ + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + + /** + * NOTE AgentUser代表一次会话记录,在上一个会话结束,并且由坐席人员点击"清除"后,会从数据库中删除 + * 此处查询到的,可能是之前的会话。其状态需要验证,所以不一定是由TA来服务本次会话。 + */ + AgentUser agentUser = cache.findOneAgentUserByUserIdAndOrgi(ctx.getOnlineUserId(), ctx.getOrgi()).orElseGet( + () -> { + /** + * NOTE 新创建的AgentUser不需要设置Status和Agentno + * 因为两个值在后面会检查,如果存在则不会申请新的Agent + */ + AgentUser p = new AgentUser( + ctx.getOnlineUserId(), + ctx.getChannel(), + ctx.getOnlineUserId(), + ctx.getOnlineUserNickname(), + ctx.getOrgi(), + ctx.getAppid()); + logger.info("[apply] create new agent user id {}", p.getId()); + return p; + }); + + + logger.info("[apply] resolve agent user id {}", agentUser.getId()); + + agentUser.setOrgi(ctx.getOrgi()); + agentUser.setUsername(resolveAgentUsername(agentUser, ctx.getOnlineUserNickname())); + agentUser.setOsname(ctx.getOsname()); + agentUser.setBrowser(ctx.getBrowser()); + agentUser.setAppid(ctx.getAppid()); + agentUser.setSessionid(ctx.getSessionid()); + + if (ctx.getIpdata() != null) { + logger.info("[apply] set IP data for agentUser {}", agentUser.getId()); + agentUser.setCountry(ctx.getIpdata().getCountry()); + agentUser.setProvince(ctx.getIpdata().getProvince()); + agentUser.setCity(ctx.getIpdata().getCity()); + if (StringUtils.isNotBlank(ctx.getIp())) { + agentUser.setRegion(ctx.getIpdata().toString() + "[" + ctx.getIp() + "]"); + } else { + agentUser.setRegion(ctx.getIpdata().toString()); + } + } + + agentUser.setOwner(ctx.getOwnerid()); // 智能IVR的 EventID + agentUser.setHeadimgurl(ctx.getOnlineUserHeadimgUrl()); + agentUser.setTitle(ctx.getTitle()); + agentUser.setUrl(ctx.getUrl()); + agentUser.setTraceid(ctx.getTraceid()); + + ctx.setAgentUser(agentUser); + next.apply(); + + logger.info( + "[apply] message text: {}, noagent {}", ctx.getMessage(), ctx.isNoagent()); + + } + + + /** + * 确定该访客的名字,优先级 + * 1. 如果AgentUser username 与 nickName 不一致,则用 agentUser username + * 2. 如果AgentUser username 与 nickName 一致,则查找 AgentUserContact对应的联系人 + * 2.1 如果联系人存在,则用联系人的名字 + * 2.2 如果联系人不存在,则使用 nickName + *

+ * TODO 此处有一些问题:如果联系人更新了名字,那么么后面TA的会话用的还是旧的名字, + * 所以,在更新联系人名字的时候,也应更新其对应的AgentUser里面的名字 + * + * @param agentUser + * @param nickname + * @return + */ + private String resolveAgentUsername(final AgentUser agentUser, final String nickname) { + if (!StringUtils.equals(agentUser.getUsername(), nickname)) { + return agentUser.getUsername(); + } + + // 查找会话联系人关联表 + AgentUserContacts agentUserContact = agentUserContactsRes.findOneByUseridAndOrgi( + agentUser.getUserid(), agentUser.getOrgi()).orElse(null); + if (agentUserContact != null) { + Contacts contact = contactsRes.findOneById(agentUserContact.getContactsid()).orElseGet(null); + if (contact != null) { + return contact.getName(); + } + } + + return nickname; + } + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisServiceMw.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisServiceMw.java new file mode 100644 index 00000000..64ad26fd --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisServiceMw.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.visitor; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.cc.acd.ACDMessageHelper; +import com.chatopera.cc.acd.ACDQueueService; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.proxy.AgentUserProxy; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 寻找或为绑定服务访客的坐席,建立双方通话 + */ +@Component +public class ACDVisServiceMw implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(ACDVisServiceMw.class); + + @Autowired + private ACDQueueService acdQueueService; + + @Autowired + private ACDMessageHelper acdMessageHelper; + + @Autowired + private AgentUserProxy agentUserProxy; + + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + ctx.setMessageType(MainContext.MessageType.STATUS.toString()); + /** + * 首先交由 IMR处理 MESSAGE指令 , 如果当前用户是在 坐席对话列表中, 则直接推送给坐席,如果不在,则执行 IMR + */ + if (StringUtils.isNotBlank(ctx.getAgentUser().getStatus())) { + // 该AgentUser已经在数据库中 + switch (MainContext.AgentUserStatusEnum.toValue(ctx.getAgentUser().getStatus())) { + case INQUENE: + logger.info("[apply] agent user is in queue"); + int queueIndex = acdQueueService.getQueueIndex( + ctx.getAgentUser().getAgentno(), ctx.getOrgi(), + ctx.getOrganid()); + ctx.setMessage( + acdMessageHelper.getQueneMessage( + queueIndex, + ctx.getChannel(), + ctx.getOrgi())); + break; + case INSERVICE: + // 该访客与坐席正在服务中,忽略新的连接 + logger.info( + "[apply] agent user {} is in service, userid {}, agentno {}", ctx.getAgentUser().getId(), + ctx.getAgentUser().getUserid(), ctx.getAgentUser().getAgentno()); + break; + case END: + logger.info("[apply] agent user is null or END"); + // 过滤坐席,获得 Agent Service + next.apply(); + if (ctx.getAgentService() != null) { + // 没有得到agent service + postResolveAgentService(ctx); + } + } + } else { + // 该AgentUser为新建 + // 过滤坐席,获得 Agent Service + next.apply(); + if (ctx.getAgentService() != null) { + // 没有得到agent service + postResolveAgentService(ctx); + } + } + } + + /** + * 根据AgentService,按照逻辑继续执行 + * + * @param ctx + */ + private void postResolveAgentService(final ACDComposeContext ctx) { + /** + * 找到空闲坐席,如果未找到坐席,则将该用户放入到 排队队列 + */ + switch (MainContext.AgentUserStatusEnum.toValue(ctx.getAgentService().getStatus())) { + case INSERVICE: + ctx.setMessage( + acdMessageHelper.getSuccessMessage( + ctx.getAgentService(), + ctx.getChannel(), + ctx.getOrgi())); + + // TODO 判断 INSERVICE 时,agentService 对应的 agentUser + logger.info( + "[apply] agent service: agentno {}, \n agentuser id {} \n user {} \n channel {} \n status {} \n queue index {}", + ctx.getAgentService().getAgentno(), ctx.getAgentService().getAgentuserid(), + ctx.getAgentService().getUserid(), + ctx.getAgentService().getChannel(), + ctx.getAgentService().getStatus(), + ctx.getAgentService().getQueneindex()); + + if (StringUtils.isNotBlank(ctx.getAgentService().getAgentuserid())) { + agentUserProxy.findOne(ctx.getAgentService().getAgentuserid()).ifPresent(p -> { + ctx.setAgentUser(p); + }); + } + + // TODO 如果是 INSERVICE 那么 agentService.getAgentuserid 就一定不能为空? +// // TODO 此处需要考虑 agentService.getAgentuserid 为空的情况 +// // 那么什么情况下,agentService.getAgentuserid为空? +// if (StringUtils.isNotBlank(agentService.getAgentuserid())) { +// logger.info("[handle] set Agent User with agentUser Id {}", agentService.getAgentuserid()); +// getAgentUserProxy().findOne(agentService.getAgentuserid()).ifPresent(p -> { +// outMessage.setChannelMessage(p); +// }); +// } else { +// logger.info("[handle] agent user id is null."); +// } + break; + case INQUENE: + if (ctx.getAgentService().getQueneindex() > 0) { + // 当前有坐席,要排队 + ctx.setMessage(acdMessageHelper.getQueneMessage( + ctx.getAgentService().getQueneindex(), + ctx.getAgentUser().getChannel(), + ctx.getOrgi())); + } else { + // TODO 什么是否返回 noAgentMessage, 是否在是 INQUENE 时 getQueneindex == 0 + // 当前没有坐席,要留言 + ctx.setMessage(acdMessageHelper.getNoAgentMessage( + ctx.getAgentService().getQueneindex(), + ctx.getChannel(), + ctx.getOrgi())); + } + break; + case END: + logger.info("[handler] should not happen for new onlineUser service request."); + default: + } + ctx.setChannelMessage(ctx.getAgentUser()); + } + + +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisSessionCfgMw.java b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisSessionCfgMw.java new file mode 100644 index 00000000..df02dc56 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/acd/visitor/ACDVisSessionCfgMw.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.acd.visitor; + +import com.chatopera.cc.acd.ACDComposeContext; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDWorkMonitor; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.model.AgentReport; +import com.chatopera.cc.model.SessionConfig; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * + */ +@Component +public class ACDVisSessionCfgMw implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(ACDVisSessionCfgMw.class); + + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + + @Override + public void apply(final ACDComposeContext ctx, final Functional next) { + SessionConfig sessionConfig = acdPolicyService.initSessionConfig( + ctx.getOrgi()); + + ctx.setSessionConfig(sessionConfig); + + // 查询就绪的坐席,如果指定技能组则按照技能组查询 + AgentReport report; + if (StringUtils.isNotBlank(ctx.getOrganid())) { + report = acdWorkMonitor.getAgentReport(ctx.getOrganid(), ctx.getOrgi()); + } else { + report = acdWorkMonitor.getAgentReport(ctx.getOrgi()); + } + + ctx.setAgentReport(report); + + // 不在工作时间段 + if (sessionConfig.isHourcheck() && !MainUtils.isInWorkingHours(sessionConfig.getWorkinghours())) { + logger.info("[apply] not in working hours"); + ctx.setMessage(sessionConfig.getNotinwhmsg()); + } else if (report.getAgents() == 0) { + // 没有就绪的坐席 + logger.info("[apply] find no agents, redirect to leave a message."); + ctx.setNoagent(true); + } else { + logger.info("[apply] find agents size {}, allocate agent in next.", report.getAgents()); + // 具备工作中的就绪坐席,进入筛选坐席 + next.apply(); + } + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/activemq/SocketioConnEventSubscription.java b/contact-center/app/src/main/java/com/chatopera/cc/activemq/SocketioConnEventSubscription.java index c9ed4de5..e2b6bce5 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/activemq/SocketioConnEventSubscription.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/activemq/SocketioConnEventSubscription.java @@ -10,12 +10,13 @@ */ package com.chatopera.cc.activemq; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDWorkMonitor; +import com.chatopera.cc.acd.ACDServiceRouter; +import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.model.AgentStatus; import com.chatopera.cc.persistence.repository.AgentStatusRepository; -import com.chatopera.cc.basic.Constants; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.slf4j.Logger; @@ -36,6 +37,12 @@ public class SocketioConnEventSubscription { private final static Logger logger = LoggerFactory.getLogger(SocketioConnEventSubscription.class); + @Autowired + private ACDServiceRouter acdServiceRouter; + + @Autowired + private ACDWorkMonitor acdWorkMonitor; + @Autowired private AgentStatusRepository agentStatusRes; @@ -58,14 +65,15 @@ public class SocketioConnEventSubscription { JsonParser parser = new JsonParser(); JsonObject j = parser.parse(payload).getAsJsonObject(); if (j.has("userId") && j.has("orgi") && j.has("isAdmin")) { - final AgentStatus agentStatus = cache.findOneAgentStatusByAgentnoAndOrig(j.get("userId").getAsString(), + final AgentStatus agentStatus = cache.findOneAgentStatusByAgentnoAndOrig( + j.get("userId").getAsString(), j.get("orgi").getAsString()); if (agentStatus != null && (!agentStatus.isConnected())) { /** * 处理该坐席为离线 */ // 重分配坐席 - if (AutomaticServiceDist.withdrawAgent(agentStatus.getOrgi(), agentStatus.getAgentno())) { + if (acdServiceRouter.withdrawAgent(agentStatus.getOrgi(), agentStatus.getAgentno())) { logger.info("[onMessage] re-allotAgent for user's visitors successfully."); } else { logger.info("[onMessage] re-allotAgent, error happens."); @@ -81,15 +89,15 @@ public class SocketioConnEventSubscription { agentStatusRes.save(agentStatus); // 记录坐席工作日志 - AutomaticServiceDist.recordAgentStatus(agentStatus.getAgentno(), - agentStatus.getUsername(), - agentStatus.getAgentno(), - j.get("isAdmin").getAsBoolean(), - agentStatus.getAgentno(), - agentStatus.getStatus(), - MainContext.AgentStatusEnum.OFFLINE.toString(), - MainContext.AgentWorkType.MEIDIACHAT.toString(), - agentStatus.getOrgi(), null); + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), + agentStatus.getUsername(), + agentStatus.getAgentno(), + j.get("isAdmin").getAsBoolean(), + agentStatus.getAgentno(), + agentStatus.getStatus(), + MainContext.AgentStatusEnum.OFFLINE.toString(), + MainContext.AgentWorkType.MEIDIACHAT.toString(), + agentStatus.getOrgi(), null); } else if (agentStatus == null) { // 该坐席已经完成离线设置 logger.info("[onMessage] agent is already offline, skip any further operations"); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/aspect/OnlineUserAspect.java b/contact-center/app/src/main/java/com/chatopera/cc/aspect/OnlineUserAspect.java index 3b4e4c33..51cb496e 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/aspect/OnlineUserAspect.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/aspect/OnlineUserAspect.java @@ -43,9 +43,9 @@ public class OnlineUserAspect { @Before("execution(* com.chatopera.cc.persistence.repository.OnlineUserRepository.save(..))") public void save(final JoinPoint joinPoint) { final OnlineUser onlineUser = (OnlineUser) joinPoint.getArgs()[0]; - logger.info( - "[save] put onlineUser id {}, status {}, invite status {}", onlineUser.getId(), onlineUser.getStatus(), - onlineUser.getInvitestatus()); +// logger.info( +// "[save] put onlineUser id {}, status {}, invite status {}", onlineUser.getId(), onlineUser.getStatus(), +// onlineUser.getInvitestatus()); if (StringUtils.isNotBlank(onlineUser.getStatus())) { switch (MainContext.OnlineUserStatusEnum.toValue(onlineUser.getStatus())) { case OFFLINE: diff --git a/contact-center/app/src/main/java/com/chatopera/cc/basic/MainContext.java b/contact-center/app/src/main/java/com/chatopera/cc/basic/MainContext.java index 51450a3f..3b487c57 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/basic/MainContext.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/basic/MainContext.java @@ -17,6 +17,7 @@ package com.chatopera.cc.basic; +import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.basic.resource.ActivityResource; import com.chatopera.cc.basic.resource.BatchResource; import com.chatopera.cc.cache.Cache; @@ -59,6 +60,8 @@ public class MainContext { private static PeerSyncIM peerSyncIM; + private static ACDServiceRouter acdServiceRouter; + static { ConvertUtils.register(new DateConverter(), java.util.Date.class); enableModule("report"); @@ -594,8 +597,6 @@ public class MainContext { } - - /** * 会话监控消息类型 */ @@ -1088,4 +1089,13 @@ public class MainContext { return modules; } + public static ACDServiceRouter getACDServiceRouter() { + if (acdServiceRouter == null) { + acdServiceRouter = getContext().getBean(ACDServiceRouter.class); + } + return acdServiceRouter; + } + + + } diff --git a/contact-center/app/src/main/java/com/chatopera/cc/basic/MainUtils.java b/contact-center/app/src/main/java/com/chatopera/cc/basic/MainUtils.java index 2dcb2aa5..53713376 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/basic/MainUtils.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/basic/MainUtils.java @@ -1396,4 +1396,34 @@ public class MainUtils { } return strb.toString(); } + + public static void putMapEntry( + Map map, String name, + String value) { + String[] newValues = null; + String[] oldValues = (String[]) (String[]) map.get(name); + if (oldValues == null) { + newValues = new String[1]; + newValues[0] = value; + } else { + newValues = new String[oldValues.length + 1]; + System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); + newValues[oldValues.length] = value; + } + map.put(name, newValues); + } + + public static byte convertHexDigit(byte b) { + if ((b >= 48) && (b <= 57)) { + return (byte) (b - 48); + } + if ((b >= 97) && (b <= 102)) { + return (byte) (b - 97 + 10); + } + if ((b >= 65) && (b <= 70)) { + return (byte) (b - 65 + 10); + } + return 0; + } + } diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/ApplicationController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/ApplicationController.java index 9b63f9fa..36b93e84 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/ApplicationController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/ApplicationController.java @@ -16,7 +16,9 @@ */ package com.chatopera.cc.controller; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDWorkMonitor; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.model.User; @@ -36,6 +38,9 @@ import java.util.TimeZone; public class ApplicationController extends Handler { private final static Logger logger = LoggerFactory.getLogger(ApplicationController.class); + @Autowired + private ACDWorkMonitor acdWorkMonitor; + @Value("${git.build.version}") private String appVersionNumber; @@ -58,7 +63,7 @@ public class ApplicationController extends Handler { User logined = super.getUser(request); TimeZone timezone = TimeZone.getDefault(); - view.addObject("agentStatusReport", AutomaticServiceDist.getAgentReport(logined.getOrgi())); + view.addObject("agentStatusReport", acdWorkMonitor.getAgentReport(logined.getOrgi())); view.addObject("tenant", super.getTenant(request)); view.addObject("istenantshare", super.isEnabletneant()); view.addObject("timeDifference", timezone.getRawOffset()); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/LoginController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/LoginController.java index 98427935..a4ffc710 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/LoginController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/LoginController.java @@ -16,13 +16,16 @@ */ package com.chatopera.cc.controller; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDWorkMonitor; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.basic.auth.AuthToken; import com.chatopera.cc.cache.Cache; -import com.chatopera.cc.model.*; +import com.chatopera.cc.model.AgentStatus; +import com.chatopera.cc.model.SystemConfig; +import com.chatopera.cc.model.User; +import com.chatopera.cc.model.UserRole; import com.chatopera.cc.persistence.repository.AgentStatusRepository; import com.chatopera.cc.persistence.repository.UserRepository; import com.chatopera.cc.persistence.repository.UserRoleRepository; @@ -84,6 +87,9 @@ public class LoginController extends Handler { @Autowired private UserProxy userProxy; + @Autowired + private ACDWorkMonitor acdWorkMonitor; + /** * 登录页面 * @@ -208,7 +214,7 @@ public class LoginController extends Handler { agentStatusRes.save(agentStatus); // 工作状态记录 - AutomaticServiceDist.recordAgentStatus(agentStatus.getAgentno(), + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), user.isAdmin(), // 0代表admin diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/admin/AdminController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/admin/AdminController.java index 4c1bbb2b..ed6e2a25 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/admin/AdminController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/admin/AdminController.java @@ -16,21 +16,23 @@ */ package com.chatopera.cc.controller.admin; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDWorkMonitor; +import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.controller.Handler; -import com.chatopera.cc.socketio.client.NettyClients; import com.chatopera.cc.model.SysDic; import com.chatopera.cc.model.User; import com.chatopera.cc.persistence.repository.OnlineUserRepository; import com.chatopera.cc.persistence.repository.SysDicRepository; import com.chatopera.cc.persistence.repository.UserEventRepository; import com.chatopera.cc.persistence.repository.UserRepository; -import com.chatopera.cc.basic.Constants; -import com.chatopera.cc.util.Menu; import com.chatopera.cc.proxy.OnlineUserProxy; +import com.chatopera.cc.socketio.client.NettyClients; +import com.chatopera.cc.util.Menu; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -47,6 +49,9 @@ import java.util.List; @Controller public class AdminController extends Handler { + @Autowired + private ACDWorkMonitor acdWorkMonitor; + @Autowired private UserRepository userRes; @@ -66,7 +71,7 @@ public class AdminController extends Handler { public ModelAndView index(ModelMap map, HttpServletRequest request) { ModelAndView view = request(super.createRequestPageTempletResponse("redirect:/")); User user = super.getUser(request); - view.addObject("agentStatusReport", AutomaticServiceDist.getAgentReport(user.getOrgi())); + view.addObject("agentStatusReport", acdWorkMonitor.getAgentReport(user.getOrgi())); view.addObject("agentStatus", cache.findOneAgentStatusByAgentnoAndOrig(user.getId(), user.getOrgi())); return view; } @@ -78,7 +83,7 @@ public class AdminController extends Handler { map.put("chatClients", NettyClients.getInstance().size()); map.put("systemCaches", cache.getSystemSizeByOrgi(MainContext.SYSTEM_ORGI)); - map.put("agentReport", AutomaticServiceDist.getAgentReport(orgi)); + map.put("agentReport", acdWorkMonitor.getAgentReport(orgi)); map.put("webIMReport", MainUtils.getWebIMReport(userEventRes.findByOrgiAndCreatetimeRange(super.getOrgi(request), MainUtils.getStartTime(), MainUtils.getEndTime()))); map.put("agents", getAgent(request).size()); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAgentUserController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAgentUserController.java index 7f754334..388447cd 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAgentUserController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAgentUserController.java @@ -16,13 +16,14 @@ */ package com.chatopera.cc.controller.api; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDMessageHelper; +import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.basic.MainContext.*; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; -import com.chatopera.cc.exception.CSKefuException; import com.chatopera.cc.controller.Handler; import com.chatopera.cc.controller.api.request.RestUtils; +import com.chatopera.cc.exception.CSKefuException; import com.chatopera.cc.model.*; import com.chatopera.cc.peer.PeerSyncIM; import com.chatopera.cc.persistence.repository.AgentServiceRepository; @@ -61,6 +62,12 @@ public class ApiAgentUserController extends Handler { private final static Logger logger = LoggerFactory.getLogger(ApiAgentUserController.class); + @Autowired + private ACDMessageHelper acdMessageHelper; + + @Autowired + private ACDServiceRouter acdServiceRouter; + @Autowired private Cache cache; @@ -201,7 +208,7 @@ public class ApiAgentUserController extends Handler { // 更新当前坐席的服务访客列表 if (currentAgentStatus != null) { cache.deleteOnlineUserIdFromAgentStatusByUseridAndAgentnoAndOrgi(userId, currentAgentno, orgi); - AutomaticServiceDist.updateAgentStatus(currentAgentStatus, orgi); + agentUserProxy.updateAgentStatus(currentAgentStatus, orgi); } if (transAgentStatus != null) { @@ -212,7 +219,7 @@ public class ApiAgentUserController extends Handler { // 转接坐席提示消息 Message outMessage = new Message(); outMessage.setMessage( - AutomaticServiceDist.getSuccessMessage(agentService, agentUser.getChannel(), orgi)); + acdMessageHelper.getSuccessMessage(agentService, agentUser.getChannel(), orgi)); outMessage.setMessageType(MediaType.TEXT.toString()); outMessage.setCalltype(CallType.IN.toString()); outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); @@ -292,7 +299,7 @@ public class ApiAgentUserController extends Handler { logined.getId(), agentUser.getAgentno()) || logined.isAdmin())) { // 删除访客-坐席关联关系,包括缓存 try { - AutomaticServiceDist.deleteAgentUser(agentUser, orgi); + acdServiceRouter.deleteAgentUser(agentUser, orgi); } catch (CSKefuException e) { // 未能删除成功 logger.error("[end]", e); @@ -322,7 +329,7 @@ public class ApiAgentUserController extends Handler { */ private JsonObject withdraw(final HttpServletRequest request, final JsonObject j) { JsonObject resp = new JsonObject(); - AutomaticServiceDist.withdrawAgent(super.getOrgi(request), super.getUser(request).getId()); + acdServiceRouter.withdrawAgent(super.getOrgi(request), super.getUser(request).getId()); resp.addProperty(RestUtils.RESP_KEY_RC, RestUtils.RESP_RC_SUCC); return resp; } diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAppsController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAppsController.java index e385ce15..6ad76dcf 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAppsController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiAppsController.java @@ -55,7 +55,8 @@ public class ApiAppsController extends Handler { @Menu(type = "apps", subtype = "apps", access = true) public ResponseEntity operations(HttpServletRequest request, @RequestBody final String body, @Valid String q) { logger.info("[operations] body {}, q {}", body, q); - final JsonObject j = StringUtils.isBlank(body) ? (new JsonObject()) : (new JsonParser()).parse(body).getAsJsonObject(); + final JsonObject j = StringUtils.isBlank(body) ? (new JsonObject()) : (new JsonParser()).parse( + body).getAsJsonObject(); JsonObject json = new JsonObject(); HttpHeaders headers = RestUtils.header(); @@ -111,7 +112,7 @@ public class ApiAppsController extends Handler { logger.info("[invite] new invite record {} of onlineUser id {} saved.", record.getId(), onlineUser.getId()); try { - OnlineUserProxy.sendWebIMClients(onlineUser.getUserid(), "invite"); + OnlineUserProxy.sendWebIMClients(onlineUser.getUserid(), "invite:" + agentno); resp.addProperty(RestUtils.RESP_KEY_RC, RestUtils.RESP_RC_SUCC); } catch (Exception e) { logger.error("[invite] error", e); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiServiceQueneController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiServiceQueneController.java index 3ecfd57b..d3f13a1d 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiServiceQueneController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/api/ApiServiceQueneController.java @@ -16,7 +16,9 @@ */ package com.chatopera.cc.controller.api; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDServiceRouter; +import com.chatopera.cc.acd.ACDWorkMonitor; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.controller.Handler; @@ -25,7 +27,7 @@ import com.chatopera.cc.model.SessionConfig; import com.chatopera.cc.model.User; import com.chatopera.cc.persistence.repository.AgentStatusRepository; import com.chatopera.cc.persistence.repository.AgentUserRepository; -import com.chatopera.cc.persistence.repository.OrganRepository; +import com.chatopera.cc.proxy.AgentUserProxy; import com.chatopera.cc.util.Menu; import com.chatopera.cc.util.RestResult; import com.chatopera.cc.util.RestResultType; @@ -51,17 +53,24 @@ import java.util.List; @RequestMapping("/api/servicequene") public class ApiServiceQueneController extends Handler { + @Autowired + private AgentUserProxy agentUserProxy; + + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + @Autowired + private ACDPolicyService acdPolicyService; + @Autowired private AgentStatusRepository agentStatusRepository; + @Autowired + private ACDServiceRouter acdServiceRouter; @Autowired private AgentUserRepository agentUserRepository; - @Autowired - private OrganRepository organRes; - - @Autowired private Cache cache; @@ -75,7 +84,7 @@ public class ApiServiceQueneController extends Handler { @Menu(type = "apps", subtype = "user", access = true) public ResponseEntity list(HttpServletRequest request) { return new ResponseEntity<>( - new RestResult(RestResultType.OK, AutomaticServiceDist.getAgentReport(super.getOrgi(request))), + new RestResult(RestResultType.OK, acdWorkMonitor.getAgentReport(super.getOrgi(request))), HttpStatus.OK); } @@ -106,7 +115,7 @@ public class ApiServiceQueneController extends Handler { agentStatus.setLogindate(new Date()); agentStatus.setSkills(logined.getSkills()); - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(super.getOrgi(request)); agentStatus.setUsers(agentUserRepository.countByAgentnoAndStatusAndOrgi( logined.getId(), @@ -130,19 +139,19 @@ public class ApiServiceQueneController extends Handler { agentStatus.setStatus(MainContext.AgentStatusEnum.READY.toString()); cache.putAgentStatusByOrgi(agentStatus, super.getOrgi(request)); - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), logined.isAdmin(), agentStatus.getAgentno(), MainContext.AgentStatusEnum.OFFLINE.toString(), MainContext.AgentStatusEnum.READY.toString(), MainContext.AgentWorkType.MEIDIACHAT.toString(), agentStatus.getOrgi(), null); - AutomaticServiceDist.allotAgent(agentStatus.getAgentno(), super.getOrgi(request)); + acdServiceRouter.allotVisitors(agentStatus.getAgentno(), super.getOrgi(request)); } } else if (StringUtils.isNotBlank(status)) { if (status.equals(MainContext.AgentStatusEnum.NOTREADY.toString())) { List agentStatusList = agentStatusRepository.findByAgentnoAndOrgi( logined.getId(), super.getOrgi(request)); for (AgentStatus temp : agentStatusList) { - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( temp.getAgentno(), temp.getUsername(), temp.getAgentno(), logined.isAdmin(), temp.getAgentno(), @@ -158,7 +167,7 @@ public class ApiServiceQueneController extends Handler { if (agentStatusList.size() > 0) { agentStatus = agentStatusList.get(0); agentStatus.setBusy(true); - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), logined.isAdmin(), agentStatus.getAgentno(), MainContext.AgentStatusEnum.READY.toString(), MainContext.AgentStatusEnum.BUSY.toString(), @@ -176,7 +185,7 @@ public class ApiServiceQueneController extends Handler { if (agentStatusList.size() > 0) { agentStatus = agentStatusList.get(0); agentStatus.setBusy(false); - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), logined.isAdmin(), agentStatus.getAgentno(), MainContext.AgentStatusEnum.BUSY.toString(), MainContext.AgentStatusEnum.READY.toString(), @@ -187,9 +196,9 @@ public class ApiServiceQueneController extends Handler { agentStatusRepository.save(agentStatus); cache.putAgentStatusByOrgi(agentStatus, super.getOrgi(request)); } - AutomaticServiceDist.allotAgent(agentStatus.getAgentno(), super.getOrgi(request)); + acdServiceRouter.allotVisitors(agentStatus.getAgentno(), super.getOrgi(request)); } - AutomaticServiceDist.broadcastAgentsStatus( + agentUserProxy.broadcastAgentsStatus( super.getOrgi(request), "agent", "api", super.getUser(request).getId()); } return new ResponseEntity<>(new RestResult(RestResultType.OK, agentStatus), HttpStatus.OK); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentAuditController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentAuditController.java index bd55c04b..7257a63d 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentAuditController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentAuditController.java @@ -17,7 +17,10 @@ package com.chatopera.cc.controller.apps; import com.alibaba.fastjson.JSONObject; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDMessageHelper; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.activemq.BrokerPublisher; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainUtils; @@ -57,6 +60,18 @@ import java.util.*; public class AgentAuditController extends Handler { private final static Logger logger = LoggerFactory.getLogger(AgentAuditController.class); + @Autowired + private ACDAgentService acdAgentService; + + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDMessageHelper acdMessageHelper; + + @Autowired + private ACDServiceRouter acdServiceRouter; + @Autowired private AgentUserRepository agentUserRes; @@ -128,7 +143,7 @@ public class AgentAuditController extends Handler { @Valid final String skill, @Valid final String agentno, @Valid String sort - ) { + ) { final String orgi = super.getOrgi(request); final User logined = super.getUser(request); logger.info("[index] skill {}, agentno {}, logined {}", skill, agentno, logined.getId()); @@ -256,7 +271,7 @@ public class AgentAuditController extends Handler { HttpServletRequest request, String id, String channel - ) throws IOException, TemplateException { + ) throws IOException, TemplateException { String mainagentuser = "/apps/cca/mainagentuser"; if (channel.equals("phone")) { mainagentuser = "/apps/cca/mainagentuser_callout"; @@ -287,11 +302,11 @@ public class AgentAuditController extends Handler { view.addObject( "agentUserMessageList", this.chatMessageRepository.findByUsessionAndOrgi(agentUser.getUserid(), orgi, - new PageRequest(0, 20, Sort.Direction.DESC, - "updatetime" - ) - ) - ); + new PageRequest(0, 20, Sort.Direction.DESC, + "updatetime" + ) + ) + ); AgentService agentService = null; if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { agentService = this.agentServiceRes.findOne(agentUser.getAgentserviceid()); @@ -320,20 +335,20 @@ public class AgentAuditController extends Handler { } view.addObject("serviceCount", Integer .valueOf(this.agentServiceRes - .countByUseridAndOrgiAndStatus(agentUser - .getUserid(), orgi, - MainContext.AgentUserStatusEnum.END - .toString()))); + .countByUseridAndOrgiAndStatus(agentUser + .getUserid(), orgi, + MainContext.AgentUserStatusEnum.END + .toString()))); view.addObject("tagRelationList", tagRelationRes.findByUserid(agentUser.getUserid())); } - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(super.getOrgi(request)); - view.addObject("sessionConfig", sessionConfig); - if (sessionConfig.isOtherquickplay()) { - view.addObject("topicList", OnlineUserProxy.search(null, orgi, super.getUser(request))); - } + view.addObject("sessionConfig", sessionConfig); + if (sessionConfig.isOtherquickplay()) { + view.addObject("topicList", OnlineUserProxy.search(null, orgi, super.getUser(request))); + } - view.addObject("tags", tagRes.findByOrgiAndTagtype(orgi, MainContext.ModelType.USER.toString())); + view.addObject("tags", tagRes.findByOrgiAndTagtype(orgi, MainContext.ModelType.USER.toString())); return view; } @@ -357,7 +372,7 @@ public class AgentAuditController extends Handler { final @Valid String agentserviceid, final @Valid String agentnoid, final @Valid String agentuserid - ) { + ) { logger.info("[transfer] userId {}, agentUser {}", userid, agentuserid); final String orgi = super.getOrgi(request); final User logined = super.getUser(request); @@ -422,7 +437,7 @@ public class AgentAuditController extends Handler { HttpServletRequest request, @Valid String agentnoid, @Valid String organ - ) { + ) { final String orgi = super.getOrgi(request); if (StringUtils.isNotBlank(organ)) { List usersids = new ArrayList(); @@ -473,7 +488,7 @@ public class AgentAuditController extends Handler { @Valid final String currentAgentnoid, @Valid final String agentno, // 会话转接给下一个坐席 @Valid final String memo - ) throws CSKefuException { + ) throws CSKefuException { final String currentAgentno = currentAgentnoid; // 当前会话坐席的agentno final String orgi = super.getOrgi(request); @@ -506,7 +521,7 @@ public class AgentAuditController extends Handler { // 更新当前坐席的服务访客列表 if (currentAgentStatus != null) { cache.deleteOnlineUserIdFromAgentStatusByUseridAndAgentnoAndOrgi(userid, currentAgentno, orgi); - AutomaticServiceDist.updateAgentStatus(currentAgentStatus, super.getOrgi(request)); + agentUserProxy.updateAgentStatus(currentAgentStatus, super.getOrgi(request)); } if (transAgentStatus != null) { @@ -518,7 +533,7 @@ public class AgentAuditController extends Handler { try { Message outMessage = new Message(); outMessage.setMessage( - AutomaticServiceDist.getSuccessMessage(agentService, agentUser.getChannel(), orgi)); + acdMessageHelper.getSuccessMessage(agentService, agentUser.getChannel(), orgi)); outMessage.setMessageType(MainContext.MediaType.TEXT.toString()); outMessage.setCalltype(MainContext.CallType.IN.toString()); outMessage.setCreatetime(MainUtils.dateFormate.format(new Date())); @@ -534,7 +549,7 @@ public class AgentAuditController extends Handler { agentUser.getUserid(), outMessage, true - ); + ); } // 通知转接消息给新坐席 @@ -544,7 +559,7 @@ public class AgentAuditController extends Handler { MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM, agentUser.getAppid(), MainContext.MessageType.NEW, agentService.getAgentno(), outMessage, true - ); + ); } catch (Exception ex) { logger.error("[transfersave]", ex); @@ -588,7 +603,7 @@ public class AgentAuditController extends Handler { logined.getId(), agentUser.getAgentno()) || logined.isAdmin())) { // 删除访客-坐席关联关系,包括缓存 try { - AutomaticServiceDist.deleteAgentUser(agentUser, orgi); + acdServiceRouter.deleteAgentUser(agentUser, orgi); } catch (CSKefuException e) { // 未能删除成功 logger.error("[end]", e); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentController.java index d3ee2e87..c708d2be 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentController.java @@ -17,7 +17,10 @@ package com.chatopera.cc.controller.apps; import com.alibaba.fastjson.JSONObject; - import com.chatopera.cc.acd.AutomaticServiceDist; + import com.chatopera.cc.acd.ACDAgentService; + import com.chatopera.cc.acd.ACDPolicyService; + import com.chatopera.cc.acd.ACDWorkMonitor; + import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.activemq.BrokerPublisher; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; @@ -72,6 +75,18 @@ static final Logger logger = LoggerFactory.getLogger(AgentController.class); + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDAgentService acdAgentService; + + @Autowired + private ACDServiceRouter acdServiceRouter; + @Autowired private ContactsRepository contactsRes; @@ -448,7 +463,7 @@ view.addObject("tagRelationList", tagRelationRes.findByUserid(agentUser.getUserid())); } - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(super.getOrgi(request)); view.addObject("sessionConfig", sessionConfig); if (sessionConfig.isOtherquickplay()) { @@ -472,7 +487,7 @@ @RequestMapping("/other/topic") @Menu(type = "apps", subtype = "othertopic") public ModelAndView othertopic(ModelMap map, HttpServletRequest request, String q) throws IOException, TemplateException { - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(super.getOrgi(request)); map.put("sessionConfig", sessionConfig); if (sessionConfig.isOtherquickplay()) { @@ -485,7 +500,7 @@ @RequestMapping("/other/topic/detail") @Menu(type = "apps", subtype = "othertopicdetail") public ModelAndView othertopicdetail(ModelMap map, HttpServletRequest request, String id) throws IOException, TemplateException { - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(super.getOrgi(request)); map.put("sessionConfig", sessionConfig); if (sessionConfig.isOtherquickplay()) { @@ -538,8 +553,8 @@ agentStatusRes.save(agentStatus); // 为该坐席分配访客 - AutomaticServiceDist.allotAgent(agentStatus.getAgentno(), orgi); - AutomaticServiceDist.recordAgentStatus(agentStatus.getAgentno(), + acdServiceRouter.allotVisitors(agentStatus.getAgentno(), orgi); + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), logined.isAdmin(), // 0代表admin @@ -575,7 +590,7 @@ cache.putAgentStatusByOrgi(agentStatus, orgi); agentStatusRes.save(agentStatus); - AutomaticServiceDist.recordAgentStatus(agentStatus.getAgentno(), + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), logined.isAdmin(), // 0代表admin @@ -603,7 +618,7 @@ logined.getId(), logined.getOrgi(), logined.getSkills()); agentStatus.setBusy(true); - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), @@ -618,7 +633,7 @@ cache.putAgentStatusByOrgi(agentStatus, super.getOrgi(request)); agentStatusRes.save(agentStatus); - AutomaticServiceDist.broadcastAgentsStatus(super.getOrgi(request), "agent", "busy", logined.getId()); + agentUserProxy.broadcastAgentsStatus(super.getOrgi(request), "agent", "busy", logined.getId()); return request(super.createRequestPageTempletResponse("/public/success")); } @@ -644,7 +659,7 @@ agentStatus.setStatus(MainContext.AgentStatusEnum.READY.toString()); // 更新工作记录 - AutomaticServiceDist.recordAgentStatus( + acdWorkMonitor.recordAgentStatus( agentStatus.getAgentno(), agentStatus.getUsername(), agentStatus.getAgentno(), @@ -661,7 +676,7 @@ agentStatusRes.save(agentStatus); // 重新分配访客给坐席 - AutomaticServiceDist.allotAgent(agentStatus.getAgentno(), super.getOrgi(request)); + acdServiceRouter.allotVisitors(agentStatus.getAgentno(), super.getOrgi(request)); return request(super.createRequestPageTempletResponse("/public/success")); } @@ -676,7 +691,7 @@ List agentServiceList = new ArrayList(); for (AgentUser agentUser : agentUserList) { if (agentUser != null && super.getUser(request).getId().equals(agentUser.getAgentno())) { - AutomaticServiceDist.deleteAgentUser(agentUser, orgi); + acdServiceRouter.deleteAgentUser(agentUser, orgi); AgentService agentService = agentServiceRes.findByIdAndOrgi(agentUser.getAgentserviceid(), orgi); if (agentService != null) { agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); @@ -712,7 +727,7 @@ logined.getId(), agentUser.getAgentno()) || logined.isAdmin())) { // 删除访客-坐席关联关系,包括缓存 try { - AutomaticServiceDist.deleteAgentUser(agentUser, orgi); + acdServiceRouter.deleteAgentUser(agentUser, orgi); } catch (CSKefuException e) { // 未能删除成功 logger.error("[end]", e); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentQualityController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentQualityController.java index f50a8d41..eacf846c 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentQualityController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentQualityController.java @@ -17,7 +17,7 @@ package com.chatopera.cc.controller.apps; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDPolicyService; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.controller.Handler; @@ -44,6 +44,8 @@ import java.util.List; @RequestMapping("/apps/quality") public class AgentQualityController extends Handler { + @Autowired + private ACDPolicyService acdPolicyService; @Autowired private QualityRepository qualityRes; @@ -60,7 +62,7 @@ public class AgentQualityController extends Handler { @RequestMapping(value = "/index") @Menu(type = "agent", subtype = "quality", access = false) public ModelAndView index(ModelMap map, HttpServletRequest request) { - map.addAttribute("sessionConfig", AutomaticServiceDist.initSessionConfig(super.getOrgi(request))); + map.addAttribute("sessionConfig", acdPolicyService.initSessionConfig(super.getOrgi(request))); map.addAttribute("qualityList", qualityRes.findByQualitytypeAndOrgi(MainContext.QualityType.CHAT.toString(), super.getOrgi(request))); map.addAttribute("tagList", tagRes.findByOrgiAndTagtype(super.getOrgi(request), MainContext.TagType.QUALITY.toString())); return request(super.createAppsTempletResponse("/apps/quality/index")); @@ -92,7 +94,7 @@ public class AgentQualityController extends Handler { if (tempList.size() > 0) { qualityRes.save(tempList); } - SessionConfig config = AutomaticServiceDist.initSessionConfig(super.getOrgi(request)); + SessionConfig config = acdPolicyService.initSessionConfig(super.getOrgi(request)); if (config != null) { if ("points".equals(request.getParameter("qualityscore"))) { config.setQualityscore("points"); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentSettingsController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentSettingsController.java index e57ad174..9a9463b4 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentSettingsController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AgentSettingsController.java @@ -16,14 +16,14 @@ */ package com.chatopera.cc.controller.apps; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.controller.Handler; import com.chatopera.cc.model.*; import com.chatopera.cc.persistence.repository.*; -import com.chatopera.cc.basic.Constants; import com.chatopera.cc.util.Menu; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -47,6 +47,9 @@ import java.util.List; @RequestMapping("/setting") public class AgentSettingsController extends Handler { + @Autowired + private ACDPolicyService acdPolicyService; + @Autowired private SessionConfigRepository sessionConfigRes; @@ -117,7 +120,7 @@ public class AgentSettingsController extends Handler { cache.putSessionConfigByOrgi(tempSessionConfig, orgi); cache.deleteSessionConfigListByOrgi(orgi); - AutomaticServiceDist.initSessionConfigList(); + acdPolicyService.initSessionConfigList(); map.put("sessionConfig", tempSessionConfig); return request(super.createRequestPageTempletResponse("redirect:/setting/agent/index.html")); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AppsController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AppsController.java index ccf2c7cc..d225a963 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AppsController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/AppsController.java @@ -16,7 +16,7 @@ */ package com.chatopera.cc.controller.apps; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDWorkMonitor; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; @@ -33,7 +33,6 @@ import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -52,6 +51,9 @@ import java.util.List; public class AppsController extends Handler { private final static Logger logger = LoggerFactory.getLogger(AppsController.class); + @Autowired + private ACDWorkMonitor acdWorkMonitor; + @Autowired private UserRepository userRes; @@ -137,7 +139,7 @@ public class AppsController extends Handler { } private void aggValues(ModelMap map, HttpServletRequest request) { - map.put("agentReport", AutomaticServiceDist.getAgentReport(super.getOrgi(request))); + map.put("agentReport", acdWorkMonitor.getAgentReport(super.getOrgi(request))); map.put( "webIMReport", MainUtils.getWebIMReport( userEventRes.findByOrgiAndCreatetimeRange(super.getOrgi(request), MainUtils.getStartTime(), diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/IMController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/IMController.java index b0dd96cc..fdf45d00 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/IMController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/IMController.java @@ -17,18 +17,20 @@ package com.chatopera.cc.controller.apps; -import com.chatopera.cc.proxy.OnlineUserProxy; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDWorkMonitor; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; import com.chatopera.cc.controller.Handler; -import com.chatopera.cc.socketio.util.RichMediaUtils; import com.chatopera.cc.model.*; import com.chatopera.cc.persistence.blob.JpaBlobHelper; import com.chatopera.cc.persistence.es.ContactsRepository; import com.chatopera.cc.persistence.repository.*; +import com.chatopera.cc.proxy.OnlineUserProxy; +import com.chatopera.cc.socketio.util.RichMediaUtils; import com.chatopera.cc.util.*; import freemarker.template.TemplateException; import org.apache.commons.io.FileUtils; @@ -68,6 +70,12 @@ import java.util.*; public class IMController extends Handler { private final static Logger logger = LoggerFactory.getLogger(IMController.class); + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + @Autowired + private ACDPolicyService acdPolicyService; + @Autowired private OnlineUserRepository onlineUserRes; @@ -539,8 +547,9 @@ public class IMController extends Handler { @Valid final String description, @Valid final String imgurl, @Valid final String pid, - @Valid final String purl) throws Exception { - logger.info("[index] orgi {}, skill {}, agent {}, traceid {}", orgi, skill, agent, traceid); + @Valid final String purl, + @Valid final boolean isInvite) throws Exception { + logger.info("[index] orgi {}, skill {}, agent {}, traceid {}, isInvite {}", orgi, skill, agent, traceid, isInvite); Map sessionMessageObj = cache.findOneSystemMapByIdAndOrgi(sessionid, orgi); if (sessionMessageObj != null) { @@ -581,7 +590,7 @@ public class IMController extends Handler { view.addObject("nickname", nickname); boolean consult = true; //是否已收集用户信息 - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); + SessionConfig sessionConfig = acdPolicyService.initSessionConfig(orgi); // 强制开启满意调查问卷 sessionConfig.setSatisfaction(true); @@ -599,6 +608,8 @@ public class IMController extends Handler { map.addAttribute("userid", userid); map.addAttribute("schema", request.getScheme()); map.addAttribute("sessionid", sessionid); + map.addAttribute("isInvite", isInvite); + view.addObject("product", product); view.addObject("description", description); @@ -640,9 +651,9 @@ public class IMController extends Handler { AgentReport report; if (invite.isSkill() && invite.isConsult_skill_fixed()) { // 绑定技能组 - report = AutomaticServiceDist.getAgentReport(invite.getConsult_skill_fixed_id(), invite.getOrgi()); + report = acdWorkMonitor.getAgentReport(invite.getConsult_skill_fixed_id(), invite.getOrgi()); } else { - report = AutomaticServiceDist.getAgentReport(invite.getOrgi()); + report = acdWorkMonitor.getAgentReport(invite.getOrgi()); } if (report.getAgents() == 0 || diff --git a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/service/ChatServiceController.java b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/service/ChatServiceController.java index 53cd9eca..e48af40e 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/service/ChatServiceController.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/controller/apps/service/ChatServiceController.java @@ -16,7 +16,8 @@ */ package com.chatopera.cc.controller.apps.service; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; import com.chatopera.cc.cache.Cache; @@ -24,6 +25,7 @@ import com.chatopera.cc.controller.Handler; import com.chatopera.cc.model.*; import com.chatopera.cc.peer.PeerSyncIM; import com.chatopera.cc.persistence.repository.*; +import com.chatopera.cc.proxy.AgentUserProxy; import com.chatopera.cc.proxy.OnlineUserProxy; import com.chatopera.cc.proxy.UserProxy; import com.chatopera.cc.socketio.message.Message; @@ -59,6 +61,15 @@ public class ChatServiceController extends Handler { private final static Logger logger = LoggerFactory.getLogger(ChatServiceController.class); + @Autowired + private AgentUserProxy agentUserProxy; + + @Autowired + private ACDAgentService acdAgentService; + + @Autowired + private ACDServiceRouter acdServiceRouter; + @Autowired private AgentServiceRepository agentServiceRes; @@ -228,13 +239,13 @@ public class ChatServiceController extends Handler { super.getUser(request).getId(), super.getOrgi(request)); if (agentStatus != null) { - AutomaticServiceDist.updateAgentStatus(agentStatus, super.getOrgi(request)); + agentUserProxy.updateAgentStatus(agentStatus, super.getOrgi(request)); } AgentStatus transAgentStatus = cache.findOneAgentStatusByAgentnoAndOrig( agentno, super.getOrgi(request)); if (transAgentStatus != null) { - AutomaticServiceDist.updateAgentStatus(transAgentStatus, super.getOrgi(request)); + agentUserProxy.updateAgentStatus(transAgentStatus, super.getOrgi(request)); agentService.setAgentno(agentno); agentService.setAgentusername(transAgentStatus.getUsername()); } @@ -279,7 +290,7 @@ public class ChatServiceController extends Handler { AgentUser agentUser = agentUserRepository.findByIdAndOrgi( agentService.getAgentuserid(), super.getOrgi(request)); if (agentUser != null) { - AutomaticServiceDist.deleteAgentUser(agentUser, user.getOrgi()); + acdServiceRouter.deleteAgentUser(agentUser, user.getOrgi()); } agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); agentServiceRes.save(agentService); @@ -320,7 +331,7 @@ public class ChatServiceController extends Handler { if (onlineUser != null) { IP ipdata = IPTools.getInstance().findGeography(onlineUser.getIp()); - OnlineUserProxy.allocateAgentService( + acdServiceRouter.allocateAgentService( onlineUser.getUserid(), onlineUser.getUsername(), user.getOrgi(), @@ -337,7 +348,9 @@ public class ChatServiceController extends Handler { null, null, agentService.getContactsid(), - onlineUser.getOwner()); + onlineUser.getOwner(), + true, + MainContext.ChatInitiatorType.AGENT.toString()); } } } @@ -384,7 +397,7 @@ public class ChatServiceController extends Handler { agentUser.setAgentno(null); agentUser.setSkill(null); agentUserRes.save(agentUser); - AutomaticServiceDist.allotAgent(agentUser, super.getOrgi(request)); + acdAgentService.allotAgent(agentUser, super.getOrgi(request)); } return request(super.createRequestPageTempletResponse("redirect:/service/quene/index.html")); } @@ -394,7 +407,7 @@ public class ChatServiceController extends Handler { public ModelAndView invite(ModelMap map, HttpServletRequest request, @Valid String id) throws Exception { AgentUser agentUser = agentUserRes.findByIdAndOrgi(id, super.getOrgi(request)); if (agentUser != null && agentUser.getStatus().equals(MainContext.AgentUserStatusEnum.INQUENE.toString())) { - AutomaticServiceDist.allotAgentForInvite(super.getUser(request).getId(), agentUser, super.getOrgi(request)); + acdServiceRouter.allotAgentForInvite(super.getUser(request).getId(), agentUser, super.getOrgi(request)); } return request(super.createRequestPageTempletResponse("redirect:/service/quene/index.html")); } @@ -438,7 +451,8 @@ public class ChatServiceController extends Handler { } cache.deleteAgentStatusByAgentnoAndOrgi(agentStatus.getAgentno(), super.getOrgi(request)); - AutomaticServiceDist.broadcastAgentsStatus(super.getOrgi(request), "agent", "offline", super.getUser(request).getId()); + agentUserProxy.broadcastAgentsStatus( + super.getOrgi(request), "agent", "offline", super.getUser(request).getId()); return request(super.createRequestPageTempletResponse("redirect:/service/agent/index.html")); } diff --git a/contact-center/app/src/main/java/com/chatopera/cc/interceptor/UserInterceptorHandler.java b/contact-center/app/src/main/java/com/chatopera/cc/interceptor/UserInterceptorHandler.java index 54156110..0f631303 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/interceptor/UserInterceptorHandler.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/interceptor/UserInterceptorHandler.java @@ -16,7 +16,6 @@ */ package com.chatopera.cc.interceptor; -import com.chatopera.cc.acd.AutomaticServiceDist; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; @@ -125,7 +124,9 @@ public class UserInterceptorHandler extends HandlerInterceptorAdapter { view.addObject("models", MainContext.getModules()); if (user != null) { - view.addObject("agentStatusReport", AutomaticServiceDist.getAgentReport(user.getOrgi())); + view.addObject( + "agentStatusReport", + MainContext.getACDServiceRouter().getAcdWorkMonitor().getAgentReport(user.getOrgi())); } /** * WebIM共享用户 diff --git a/contact-center/app/src/main/java/com/chatopera/cc/model/AgentStatus.java b/contact-center/app/src/main/java/com/chatopera/cc/model/AgentStatus.java index 0c98486a..6a9bca9b 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/model/AgentStatus.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/model/AgentStatus.java @@ -16,11 +16,12 @@ */ package com.chatopera.cc.model; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDPolicyService; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.Constants; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.GenericGenerator; +import org.springframework.beans.factory.annotation.Autowired; import javax.persistence.*; import java.util.Date; @@ -161,7 +162,7 @@ public class AgentStatus implements java.io.Serializable, Comparable compareByCreateTime = (OnlineUser o1, OnlineUser o2) -> o1.getCreatetime().compareTo( - o2.getCreatetime()); - /** * @param id * @return @@ -725,7 +713,7 @@ public class OnlineUserProxy { public static String getKeyword(String url) { Map values = new HashMap(); try { - parseParameters(values, url, "UTF-8"); + OnlineUserUtils.parseParameters(values, url, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } @@ -750,505 +738,6 @@ public class OnlineUserProxy { return source; } - /** - * 在访客启动聊天窗口后,建立访客和坐席的连接关系 - * 1)在inMessage中如果绑定了坐席,就联系该坐席服务这个访客 - * 2)在inMessage中如果没有绑定坐席,就寻找一个符合要求的坐席(比如技能组) - * 找不到坐席时进入排队,找到坐席通知双方加入会话 - * - * @param agentUser 预连接的坐席人员 - * @return - */ - private static Optional dispatchAgentService(final AgentUser agentUser) { - Message result = new Message(); - AgentService agentService = null; - result.setOrgi(agentUser.getOrgi()); - result.setMessageType(MainContext.MessageType.STATUS.toString()); - result.setAgentUser(agentUser); - - /** - * 首先交由 IMR处理 MESSAGE指令 , 如果当前用户是在 坐席对话列表中, 则直接推送给坐席,如果不在,则执行 IMR - */ - if (agentUser != null && StringUtils.isNotBlank(agentUser.getStatus())) { - switch (MainContext.AgentUserStatusEnum.toValue(agentUser.getStatus())) { - case INQUENE: - int queueIndex = AutomaticServiceDist.getQueueIndex( - agentUser.getAgentno(), agentUser.getOrgi(), - agentUser.getSkill()); - result.setMessage( - AutomaticServiceDist.getQueneMessage(queueIndex, agentUser.getChannel(), - agentUser.getOrgi())); - break; - case INSERVICE: - // 该访客与坐席正在服务中,忽略新的连接 - logger.info( - "[handler] agent user {} is in service, userid {}, agentno {}", agentUser.getId(), - agentUser.getUserid(), agentUser.getAgentno()); - break; - } - } else if ((agentService = AutomaticServiceDist.allotAgent( - agentUser, agentUser.getOrgi())) != null) { - /** - * 找到空闲坐席,如果未找到坐席,则将该用户放入到 排队队列 - */ - switch (MainContext.AgentUserStatusEnum.toValue(agentService.getStatus())) { - case INSERVICE: - result.setMessage( - AutomaticServiceDist.getSuccessMessage(agentService, agentUser.getChannel(), - agentUser.getOrgi())); - - // TODO 判断 INSERVICE 时,agentService 对应的 agentUser - logger.info("[handle] agent service: agentno {}", agentService.getAgentno()); - logger.info("[handle] agent service: agentuser id {}", agentService.getAgentuserid()); - logger.info( - "[handle] agent service: user {}, channle {}", agentService.getUserid(), - agentService.getChannel()); - logger.info("[handle] agent service: status {}, queue index {}", agentService.getStatus(), - agentService.getQueneindex()); - - if (StringUtils.isNotBlank(agentService.getAgentuserid())) { - getAgentUserProxy().findOne(agentService.getAgentuserid()).ifPresent(p -> { - result.setAgentUser(p); - }); - } - - // TODO 如果是 INSERVICE 那么 agentService.getAgentuserid 就一定不能为空? -// // TODO 此处需要考虑 agentService.getAgentuserid 为空的情况 -// // 那么什么情况下,agentService.getAgentuserid为空? -// if (StringUtils.isNotBlank(agentService.getAgentuserid())) { -// logger.info("[handle] set Agent User with agentUser Id {}", agentService.getAgentuserid()); -// getAgentUserProxy().findOne(agentService.getAgentuserid()).ifPresent(p -> { -// outMessage.setChannelMessage(p); -// }); -// } else { -// logger.info("[handle] agent user id is null."); -// } - break; - case INQUENE: - if (agentService.getQueneindex() > 0) { - // 当前有坐席,要排队 - result.setMessage(AutomaticServiceDist.getQueneMessage( - agentService.getQueneindex(), - agentUser.getChannel(), - agentUser.getOrgi())); - } else { - // TODO 什么是否返回 noAgentMessage, 是否在是 INQUENE 时 getQueneindex == 0 - // 当前没有坐席,要留言 - result.setMessage(AutomaticServiceDist.getNoAgentMessage( - agentService.getQueneindex(), - agentUser.getChannel(), - agentUser.getOrgi())); - } - break; - case END: - logger.info("[handler] should not happen for new onlineUser service request."); - default: - } - - result.setAgentService(agentService); - } - - return Optional.ofNullable(result); - } - - /** - * 为新增加的访客会话分配坐席和开启访客与坐席的对话 - * - * @param onlineUserId - * @param nickname - * @param orgi - * @param session - * @param appid - * @param ip - * @param osname - * @param browser - * @param headimg - * @param ipdata - * @param channel - * @param skill - * @param agent - * @param title - * @param url - * @param traceid - * @param eventid - * @return - * @throws Exception - */ - public static Message allocateAgentService( - final String onlineUserId, - final String nickname, - final String orgi, - final String session, - final String appid, - final String ip, - final String osname, - final String browser, - final String headimg, - final IP ipdata, - final String channel, - final String skill, - final String agent, - final String title, - final String url, - final String traceid, - final String eventid) { - logger.info( - "[allocateAgentService] user {}, appid {}, agent {}, skill {}, nickname {}", onlineUserId, appid, - agent, - skill, - nickname); - // 坐席服务请求,分配 坐席 - final Message result = new Message(); - - /** - * NOTE AgentUser代表一次会话记录,在上一个会话结束,并且由坐席人员点击"清除"后,会从数据库中删除 - * 此处查询到的,可能是之前的会话。其状态需要验证,所以不一定是由TA来服务本次会话。 - */ - AgentUser agentUser = getCache().findOneAgentUserByUserIdAndOrgi(onlineUserId, orgi).orElseGet(() -> { - /** - * NOTE 新创建的AgentUser不需要设置Status和Agentno - * 因为两个值在后面会检查,如果存在则不会申请新的Agent - */ - AgentUser p = new AgentUser( - onlineUserId, - channel, - onlineUserId, - nickname, - orgi, - appid); - logger.info("[allocateAgentService] create new agent user id {}", p.getId()); - return p; - }); - - logger.info("[allocateAgentService] resolve agent user id {}", agentUser.getId()); - - agentUser.setUsername(resolveAgentUsername(agentUser, nickname)); - - agentUser.setOsname(osname); - agentUser.setBrowser(browser); - agentUser.setAppid(appid); - agentUser.setSessionid(session); - - if (ipdata != null) { - logger.info("[allocateAgentService] set IP data for agentUser {}", agentUser.getId()); - agentUser.setCountry(ipdata.getCountry()); - agentUser.setProvince(ipdata.getProvince()); - agentUser.setCity(ipdata.getCity()); - if (StringUtils.isNotBlank(ip)) { - agentUser.setRegion(ipdata.toString() + "[" + ip + "]"); - } else { - agentUser.setRegion(ipdata.toString()); - } - } - - agentUser.setOwner(eventid); // 智能IVR的 EventID - agentUser.setHeadimgurl(headimg); - agentUser.setStatus(null); // 修改状态 - agentUser.setTitle(title); - agentUser.setUrl(url); - agentUser.setTraceid(traceid); - - /** - * 访客新上线的请求 - */ - /** - * 技能组 和 坐席 - */ - if (StringUtils.isNotBlank(skill)) { - // 绑定技能组 - agentUser.setSkill(skill); - } else if (StringUtils.isNotBlank(agent)) { - // 绑定坐席 - agentUser.setAgentno(agent); - agentUser.setAgentname(getUserRes().findOne(agent).getUname()); - } else { - /** - * NOTE 处理和"邀请"的关联 - * 要关联访客与发出邀请的坐席 - * 当访客接受邀请后,让该坐席与之对话 - * 方案是从数据库InviteRecord查询最近10条,然后时间降序匹配在线客服进行发送 - */ - // 根据邀请信息锁定目标坐席 - // 从邀请信息中查看,是否有Agent - // 增加时间校验,如果这个邀请是很久之前的,就忽略 - logger.info("[allocateAgentService] process invite events"); - final Date threshold = new Date(System.currentTimeMillis() - Constants.WEBIM_AGENT_INVITE_TIMEOUT); - Page inviteRecords = getInviteRecordRes().findByUseridAndOrgiAndResultAndCreatetimeGreaterThan( - onlineUserId, - orgi, - MainContext.OnlineUserInviteStatus.ACCEPT.toString(), - threshold, - new PageRequest(0, 10, Sort.Direction.DESC, "createtime")); - logger.info("[allocateAgentService] get inviteRecords size {}", inviteRecords.getContent().size()); - - for (final InviteRecord inviteRecord : inviteRecords.getContent()) { - // most recent invite - // 判断该坐席是否在线,就绪 - // TODO 此处还需要限制技能组,即在有请求技能组的前提下,确认该坐席属于这个技能组 - final AgentStatus as = cache.findOneAgentStatusByAgentnoAndOrig( - inviteRecord.getAgentno(), inviteRecord.getOrgi()); - if (as != null && - StringUtils.equals(MainContext.AgentStatusEnum.READY.toString(), as.getStatus()) && - (!as.isBusy())) { // 该坐席就绪且置闲 - logger.info( - "[allocateAgentService] find an agent {} for user {} with InviteRecord {}", - inviteRecord.getAgentno(), inviteRecord.getUserid(), inviteRecord.getId()); - agentUser.setAgentno(inviteRecord.getAgentno()); - agentUser.setAgentname(getUserRes().findOne(inviteRecord.getAgentno()).getUname()); - break; - } - } - } - - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); - AgentReport report; - if (StringUtils.isNotBlank(skill)) { - report = AutomaticServiceDist.getAgentReport(skill, orgi); - } else { - report = AutomaticServiceDist.getAgentReport(orgi); - } - - if (sessionConfig.isHourcheck() && !MainUtils.isInWorkingHours(sessionConfig.getWorkinghours())) { - result.setMessage(sessionConfig.getNotinwhmsg()); - } else { - if (report.getAgents() == 0) { - result.setNoagent(true); - } - // 寻找或为绑定服务访客的坐席,建立双方通话 - dispatchAgentService(agentUser).ifPresent(p -> { - result.setMessage(p.getMessage()); - // 为新访客找到了服务坐席 - result.setAgentService(p.getAgentService()); - result.setChannelMessage(p.getAgentUser()); - result.setAgentUser(p.getAgentUser()); - }); - } - - return result; - } - - /** - * 确定该访客的名字,优先级 - * 1. 如果AgentUser username 与 nickName 不一致,则用 agentUser username - * 2. 如果AgentUser username 与 nickName 一致,则查找 AgentUserContact对应的联系人 - * 2.1 如果联系人存在,则用联系人的名字 - * 2.2 如果联系人不存在,则使用 nickName - * - * TODO 此处有一些问题:如果联系人更新了名字,那么么后面TA的会话用的还是旧的名字, - * 所以,在更新联系人名字的时候,也应更新其对应的AgentUser里面的名字 - * @param agentUser - * @param nickname - * @return - */ - private static String resolveAgentUsername(final AgentUser agentUser, final String nickname) { - if (!StringUtils.equals(agentUser.getUsername(), nickname)) { - return agentUser.getUsername(); - } - - // 查找会话联系人关联表 - AgentUserContacts agentUserContact = getAgentUserContactsRes().findOneByUseridAndOrgi( - agentUser.getUserid(), agentUser.getOrgi()).orElse(null); - if (agentUserContact != null) { - Contacts contact = getContactsRes().findOneById(agentUserContact.getContactsid()).orElseGet(null); - if (contact != null) { - return contact.getName(); - } - } - - return nickname; - } - - /** - * @param userid - * @param orgi - * @param session - * @param appid - * @param ip - * @param osname - * @param browser - * @param channel - * @param skill - * @param agent - * @param nickname - * @param title - * @param url - * @param traceid - * @param initiator - * @return - * @throws Exception - */ - - public static Message allocateAgentService( - String userid, - String orgi, - String session, - String appid, - String ip, - String osname, - String browser, - String channel, - String skill, - String agent, - String nickname, - String title, - String url, - String traceid, - String initiator) { - IP ipdata = null; - if (StringUtils.isNotBlank(ip)) { - ipdata = IPTools.getInstance().findGeography(ip); - logger.info("[allocateAgentService] find ipdata {}", ipdata.toString()); - } else { - logger.info("[allocateAgentService] no IP present"); - } - - if (StringUtils.isBlank(nickname)) { - logger.info("[allocateAgentService] reset nickname as it does not present."); - nickname = "Guest_" + userid; - } - - return allocateAgentService( - userid, nickname, orgi, session, appid, ip, osname, browser, "", ipdata, channel, skill, agent, title, - url, traceid, session); - } - - /** - * Create agentuser object for Wechat Channel - * - * @param openid - * @param nickname - * @param orgi - * @param session - * @param appid - * @param headimg - * @param country - * @param province - * @param city - * @param channel - * @param skill - * @param agent - * @param initiator - * @return - * @throws Exception - */ - public static Message allocateAgentService( - String openid, - String nickname, - String orgi, - String session, - String appid, - String headimg, - String country, - String province, - String city, - String channel, - String skill, - String agent, - String initiator) throws Exception { - IP ipdata = new IP(); - ipdata.setCountry(country); - ipdata.setProvince(province); - ipdata.setCity(city); - return allocateAgentService( - openid, nickname, orgi, session, appid, null, null, null, headimg, ipdata, channel, skill, agent, null, - null, null, session); - } - - public static void parseParameters( - Map map, String data, - String encoding) throws UnsupportedEncodingException { - if ((data == null) || (data.length() <= 0)) { - return; - } - - byte[] bytes = null; - try { - if (encoding == null) { - bytes = data.getBytes(); - } else { - bytes = data.getBytes(encoding); - } - - } catch (UnsupportedEncodingException uee) { - } - parseParameters(map, bytes, encoding); - } - - public static void parseParameters( - Map map, byte[] data, - String encoding) throws UnsupportedEncodingException { - if ((data != null) && (data.length > 0)) { - int ix = 0; - int ox = 0; - String key = null; - String value = null; - while (ix < data.length) { - byte c = data[(ix++)]; - switch ((char) c) { - case '&': - value = new String(data, 0, ox, encoding); - if (key != null) { - putMapEntry(map, key, value); - key = null; - } - ox = 0; - break; - case '=': - if (key == null) { - key = new String(data, 0, ox, encoding); - ox = 0; - } else { - data[(ox++)] = c; - } - break; - case '+': - data[(ox++)] = 32; - break; - case '%': - data[(ox++)] = (byte) ((convertHexDigit(data[(ix++)]) << 4) + convertHexDigit(data[(ix++)])); - - break; - default: - data[(ox++)] = c; - } - } - - if (key != null) { - value = new String(data, 0, ox, encoding); - putMapEntry(map, key, value); - } - } - } - - private static void putMapEntry( - Map map, String name, - String value) { - String[] newValues = null; - String[] oldValues = (String[]) (String[]) map.get(name); - if (oldValues == null) { - newValues = new String[1]; - newValues[0] = value; - } else { - newValues = new String[oldValues.length + 1]; - System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); - newValues[oldValues.length] = value; - } - map.put(name, newValues); - } - - private static byte convertHexDigit(byte b) { - if ((b >= 48) && (b <= 57)) { - return (byte) (b - 48); - } - if ((b >= 97) && (b <= 102)) { - return (byte) (b - 97 + 10); - } - if ((b >= 65) && (b <= 70)) { - return (byte) (b - 65 + 10); - } - return 0; - } - /** * 发送邀请 * @@ -1357,7 +846,8 @@ public class OnlineUserProxy { public static List search(String q, String orgi, User user) throws IOException, TemplateException { List otherMessageItemList = null; String param = ""; - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); + SessionConfig sessionConfig = MainContext.getACDServiceRouter().getAcdPolicyService().initSessionConfig( + orgi); if (StringUtils.isNotBlank(sessionConfig.getOqrsearchurl())) { Template templet = MainUtils.getTemplate(sessionConfig.getOqrsearchinput()); Map values = new HashMap(); @@ -1416,7 +906,8 @@ public class OnlineUserProxy { public static OtherMessageItem detail(String id, String orgi, User user) throws IOException, TemplateException { OtherMessageItem otherMessageItem = null; String param = ""; - SessionConfig sessionConfig = AutomaticServiceDist.initSessionConfig(orgi); + SessionConfig sessionConfig = MainContext.getACDServiceRouter().getAcdPolicyService().initSessionConfig( + orgi); if (StringUtils.isNotBlank(sessionConfig.getOqrdetailinput())) { Template templet = MainUtils.getTemplate(sessionConfig.getOqrdetailinput()); Map values = new HashMap(); @@ -1510,13 +1001,6 @@ public class OnlineUserProxy { return cache; } - private static AgentUserProxy getAgentUserProxy() { - if (agentUserProxy == null) { - agentUserProxy = MainContext.getContext().getBean(AgentUserProxy.class); - } - return agentUserProxy; - } - private static ConsultInviteRepository getConsultInviteRes() { if (consultInviteRes == null) { consultInviteRes = MainContext.getContext().getBean(ConsultInviteRepository.class); @@ -1524,13 +1008,6 @@ public class OnlineUserProxy { return consultInviteRes; } - private static InviteRecordRepository getInviteRecordRes() { - if (inviteRecordRes == null) { - inviteRecordRes = MainContext.getContext().getBean(InviteRecordRepository.class); - } - return inviteRecordRes; - } - private static OnlineUserHisRepository getOnlineUserHisRes() { if (onlineUserHisRes == null) { onlineUserHisRes = MainContext.getContext().getBean(OnlineUserHisRepository.class); diff --git a/contact-center/app/src/main/java/com/chatopera/cc/schedule/WebIMTask.java b/contact-center/app/src/main/java/com/chatopera/cc/schedule/WebIMTask.java index b836d598..9ba2a57b 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/schedule/WebIMTask.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/schedule/WebIMTask.java @@ -15,7 +15,8 @@ */ package com.chatopera.cc.schedule; -import com.chatopera.cc.acd.AutomaticServiceDist; +import com.chatopera.cc.acd.ACDPolicyService; +import com.chatopera.cc.acd.ACDServiceRouter; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; @@ -42,7 +43,6 @@ import org.springframework.scheduling.annotation.Scheduled; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; @Configuration @EnableScheduling @@ -50,6 +50,12 @@ public class WebIMTask { private final static Logger logger = LoggerFactory.getLogger(WebIMTask.class); + @Autowired + private ACDPolicyService acdPolicyService; + + @Autowired + private ACDServiceRouter acdServiceRouter; + @Autowired private AgentUserTaskRepository agentUserTaskRes; @@ -70,7 +76,7 @@ public class WebIMTask { @Scheduled(fixedDelay = 5000, initialDelay = 20000) // 处理超时消息,每5秒执行一次 public void task() { - final List sessionConfigList = AutomaticServiceDist.initSessionConfigList(); + final List sessionConfigList = acdPolicyService.initSessionConfigList(); if (sessionConfigList != null && sessionConfigList.size() > 0 && MainContext.getContext() != null) { for (final SessionConfig sessionConfig : sessionConfigList) { if (sessionConfig.isSessiontimeout()) { //设置了启用 超时提醒 @@ -104,7 +110,7 @@ public class WebIMTask { sessionConfig.getServicename(), p, agentStatus, task); try { - AutomaticServiceDist.serviceFinish(p, task.getOrgi()); + acdServiceRouter.serviceFinish(p, task.getOrgi()); } catch (Exception e) { logger.warn("[task] exception: ", e); } @@ -130,7 +136,7 @@ public class WebIMTask { sessionConfig, sessionConfig.getRetimeoutmsg(), agentStatus.getUsername(), p, agentStatus, task); try { - AutomaticServiceDist.serviceFinish(p, task.getOrgi()); + acdServiceRouter.serviceFinish(p, task.getOrgi()); } catch (Exception e) { logger.warn("[task] exception: ", e); } @@ -152,7 +158,7 @@ public class WebIMTask { sessionConfig, sessionConfig.getQuenetimeoutmsg(), sessionConfig.getServicename(), p, null, task); try { - AutomaticServiceDist.serviceFinish(p, task.getOrgi()); + acdServiceRouter.serviceFinish(p, task.getOrgi()); } catch (Exception e) { logger.warn("[task] exception: ", e); } @@ -165,11 +171,11 @@ public class WebIMTask { @Scheduled(fixedDelay = 5000, initialDelay = 20000) // 每5秒执行一次 public void agent() { - List sessionConfigList = AutomaticServiceDist.initSessionConfigList(); + List sessionConfigList = acdPolicyService.initSessionConfigList(); if (sessionConfigList != null && sessionConfigList.size() > 0) { for (final SessionConfig sessionConfig : sessionConfigList) { // ? 为什么还要重新取一次? -// sessionConfig = AutomaticServiceDist.initSessionConfig(sessionConfig.getOrgi()); +// sessionConfig = automaticServiceDist.initSessionConfig(sessionConfig.getOrgi()); if (sessionConfig != null && MainContext.getContext() != null && sessionConfig.isAgentreplaytimeout()) { List agentUserTask = agentUserTaskRes.findByLastgetmessageLessThanAndStatusAndOrgi( MainUtils.getLastTime(sessionConfig.getAgenttimeout()), diff --git a/contact-center/app/src/main/java/com/chatopera/cc/socketio/handler/IMEventHandler.java b/contact-center/app/src/main/java/com/chatopera/cc/socketio/handler/IMEventHandler.java index 0294b73d..e936b2b9 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/socketio/handler/IMEventHandler.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/socketio/handler/IMEventHandler.java @@ -16,14 +16,12 @@ */ package com.chatopera.cc.socketio.handler; -import com.chatopera.cc.acd.AutomaticServiceDist; import com.chatopera.cc.basic.MainContext; -import com.chatopera.cc.basic.MainContext.ReceiverType; -import com.chatopera.cc.basic.MainContext.MessageType; -import com.chatopera.cc.basic.MainContext.ChannelType; import com.chatopera.cc.basic.MainContext.CallType; +import com.chatopera.cc.basic.MainContext.ChannelType; +import com.chatopera.cc.basic.MainContext.MessageType; +import com.chatopera.cc.basic.MainContext.ReceiverType; import com.chatopera.cc.basic.MainUtils; -import com.chatopera.cc.model.AgentService; import com.chatopera.cc.model.Contacts; import com.chatopera.cc.model.CousultInvite; import com.chatopera.cc.persistence.repository.AgentServiceRepository; @@ -35,6 +33,8 @@ import com.chatopera.cc.socketio.message.ChatMessage; import com.chatopera.cc.socketio.message.Message; import com.chatopera.cc.socketio.util.HumanUtils; import com.chatopera.cc.socketio.util.IMServiceUtils; +import com.chatopera.cc.util.IP; +import com.chatopera.cc.util.IPTools; import com.corundumstudio.socketio.AckRequest; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOServer; @@ -47,7 +47,6 @@ import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.util.Date; -import java.util.List; public class IMEventHandler { private final static Logger logger = LoggerFactory.getLogger(IMEventHandler.class); @@ -71,27 +70,40 @@ public class IMEventHandler { final String user = client.getHandshakeData().getSingleUrlParam("userid"); final String orgi = client.getHandshakeData().getSingleUrlParam("orgi"); final String session = MainUtils.getContextID(client.getHandshakeData().getSingleUrlParam("session")); + // 渠道标识 final String appid = client.getHandshakeData().getSingleUrlParam("appid"); + // 要求目标坐席服务 final String agent = client.getHandshakeData().getSingleUrlParam("agent"); + // 要求目标技能组服务 final String skill = client.getHandshakeData().getSingleUrlParam("skill"); + // 是否是邀请后加入会话 + final boolean isInvite = StringUtils.equalsIgnoreCase( + client.getHandshakeData().getSingleUrlParam("isInvite"), "true"); final String title = client.getHandshakeData().getSingleUrlParam("title"); final String url = client.getHandshakeData().getSingleUrlParam("url"); final String traceid = client.getHandshakeData().getSingleUrlParam("traceid"); - final String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); + String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); final String osname = client.getHandshakeData().getSingleUrlParam("osname"); final String browser = client.getHandshakeData().getSingleUrlParam("browser"); logger.info( - "[onConnect] user {}, orgi {}, session {}, appid {}, agent {}, skill {}, title {}, url {}, traceid {}, nickname {}", - user, orgi, session, appid, agent, skill, title, url, traceid, nickname); + "[onConnect] user {}, orgi {}, session {}, appid {}, agent {}, skill {}, title {}, url {}, traceid {}, nickname {}, isInvite {}", + user, orgi, session, appid, agent, skill, title, url, traceid, nickname, isInvite); // save connection info client.set("session", session); client.set("userid", user); client.set("appid", appid); + client.set("isInvite", isInvite); + + // 保证传入的Nickname不是null + if (StringUtils.isBlank(nickname)) { + logger.info("[onConnect] reset nickname as it does not present."); + nickname = "Guest_" + user; + } if (StringUtils.isNotBlank(user)) { InetSocketAddress address = (InetSocketAddress) client.getRemoteAddress(); @@ -107,26 +119,35 @@ public class IMEventHandler { */ IMServiceUtils.shiftOpsType(user, orgi, MainContext.OptType.HUMAN); + IP ipdata = null; + if ((StringUtils.isNotBlank(ip))) { + ipdata = IPTools.getInstance().findGeography(ip); + } + /** * 用户进入到对话连接 , 排队用户请求 , 如果返回失败, * 表示当前坐席全忙,用户进入排队状态,当前提示信息 显示 当前排队的队列位置, * 不可进行对话,用户发送的消息作为留言处理 */ - Message agentServiceMessage = OnlineUserProxy.allocateAgentService( + Message agentServiceMessage = MainContext.getACDServiceRouter().allocateAgentService( user, + nickname, orgi, session, appid, ip, osname, browser, + "", + ipdata, MainContext.ChannelType.WEBIM.toString(), skill, agent, - nickname, title, url, traceid, + user, + isInvite, MainContext.ChatInitiatorType.USER.toString()); if (agentServiceMessage != null && StringUtils.isNotBlank( @@ -190,7 +211,7 @@ public class IMEventHandler { * 用户主动断开服务 */ MainContext.getCache().findOneAgentUserByUserIdAndOrgi(user, orgi).ifPresent(p -> { - AutomaticServiceDist.serviceFinish(p + MainContext.getACDServiceRouter().serviceFinish(p , orgi); }); } catch (Exception e) { diff --git a/contact-center/app/src/main/java/com/chatopera/cc/socketio/message/Message.java b/contact-center/app/src/main/java/com/chatopera/cc/socketio/message/Message.java index db04987f..698e2076 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/socketio/message/Message.java +++ b/contact-center/app/src/main/java/com/chatopera/cc/socketio/message/Message.java @@ -18,6 +18,7 @@ package com.chatopera.cc.socketio.message; import com.chatopera.cc.model.*; +import com.chatopera.compose4j.AbstractContext; import java.io.Serializable; import java.util.List; @@ -25,7 +26,7 @@ import java.util.List; /** * 发送消息的高级封装 */ -public class Message implements java.io.Serializable { +public class Message extends AbstractContext { public String id; private String orgi; // 租户 diff --git a/contact-center/app/src/main/java/com/chatopera/cc/util/OnlineUserUtils.java b/contact-center/app/src/main/java/com/chatopera/cc/util/OnlineUserUtils.java new file mode 100644 index 00000000..e1cef5c2 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/util/OnlineUserUtils.java @@ -0,0 +1,77 @@ +package com.chatopera.cc.util; + +import com.chatopera.cc.basic.MainUtils; + +import java.io.UnsupportedEncodingException; +import java.util.Map; + +public class OnlineUserUtils { + + public static void parseParameters( + Map map, String data, + String encoding) throws UnsupportedEncodingException { + if ((data == null) || (data.length() <= 0)) { + return; + } + + byte[] bytes = null; + try { + if (encoding == null) { + bytes = data.getBytes(); + } else { + bytes = data.getBytes(encoding); + } + + } catch (UnsupportedEncodingException uee) { + } + parseParameters(map, bytes, encoding); + } + + + public static void parseParameters( + Map map, byte[] data, + String encoding) throws UnsupportedEncodingException { + if ((data != null) && (data.length > 0)) { + int ix = 0; + int ox = 0; + String key = null; + String value = null; + while (ix < data.length) { + byte c = data[(ix++)]; + switch ((char) c) { + case '&': + value = new String(data, 0, ox, encoding); + if (key != null) { + MainUtils.putMapEntry(map, key, value); + key = null; + } + ox = 0; + break; + case '=': + if (key == null) { + key = new String(data, 0, ox, encoding); + ox = 0; + } else { + data[(ox++)] = c; + } + break; + case '+': + data[(ox++)] = 32; + break; + case '%': + data[(ox++)] = (byte) ((MainUtils.convertHexDigit( + data[(ix++)]) << 4) + MainUtils.convertHexDigit(data[(ix++)])); + + break; + default: + data[(ox++)] = c; + } + } + + if (key != null) { + value = new String(data, 0, ox, encoding); + MainUtils.putMapEntry(map, key, value); + } + } + } +} diff --git a/contact-center/app/src/main/resources/templates/apps/im/index.html b/contact-center/app/src/main/resources/templates/apps/im/index.html index a2958d60..1680c138 100644 --- a/contact-center/app/src/main/resources/templates/apps/im/index.html +++ b/contact-center/app/src/main/resources/templates/apps/im/index.html @@ -563,7 +563,7 @@ var hostname = location.hostname ; var protocol = window.location.protocol.replace(/:/g,''); var username = encodeURIComponent("${username}"); - var socket = io(protocol + '://'+hostname+':${port}/im/user?userid=${userid!''}&orgi=${orgi!''}&session=${sessionid!''}&appid=${appid!''}&osname=${(osname!'')?url}&browser=${(browser!'')?url}<#if skill??>&skill=${skill}<#if username??>&nickname='+username+'<#if agent??>&agent=${agent}<#if title??>&title=${title?url}<#if traceid??>&url=${url?url}<#if traceid??>&traceid=${traceid}', {transports: ['websocket', 'polling']}); + var socket = io(protocol + '://'+hostname+':${port}/im/user?userid=${userid!''}&orgi=${orgi!''}&session=${sessionid!''}&appid=${appid!''}&osname=${(osname!'')?url}&browser=${(browser!'')?url}<#if skill??>&skill=${skill}<#if username??>&nickname='+username+'<#if agent??>&agent=${agent}<#if title??>&title=${title?url}<#if traceid??>&url=${url?url}<#if traceid??>&traceid=${traceid}<#if isInvite??>&isInvite=${isInvite}', {transports: ['websocket', 'polling']}); console.log('connect debug', protocol, hostname); diff --git a/contact-center/app/src/main/resources/templates/apps/im/point.html b/contact-center/app/src/main/resources/templates/apps/im/point.html index 9e19c0db..b94f5659 100644 --- a/contact-center/app/src/main/resources/templates/apps/im/point.html +++ b/contact-center/app/src/main/resources/templates/apps/im/point.html @@ -146,6 +146,7 @@ location.reload(); var protocol = window.location.protocol.replace(/:/g,''); var cskefu = { + service: {agentno: null}, time : new Date().getTime(), in: protocol + "://${hostname!''}<#if port?? && port != 80>:${port!''}/im/${appid!''}/userlist.html?appid=${appid!''}<#if aiid??>&aiid=${aiid}&orgi=${orgi!''}&client=${client}" , url: protocol + "://${hostname!''}<#if port?? && port != 80>:${port!''}/im/online?appid=${appid!''}&orgi=${orgi!''}<#if aiid??>&aiid=${aiid}&client=${client}" , @@ -176,13 +177,15 @@ var cskefu = { if (xhr.readyState == 4) { var status = xhr.status; if (status >= 200 && status < 300) { - var event = xhr.responseText ; + var event = xhr.responseText; if(event && event.indexOf('invite') >= 0){ - cskefu.writeinvite() ; + var agentno = event.substring(event.lastIndexOf(":") + 1).trim(); + cskefu.service.agentno = agentno; + cskefu.writeinvite(); }else if(event && event.indexOf('refuse') >= 0){ cskefu.refuseInvite() ; }else if(event && event.indexOf('accept') >= 0){ - cskefu.acceptInvite() ; + cskefu.acceptInvite(); } if(success){ success(event); @@ -281,7 +284,7 @@ var cskefu = { "" ; <#if webimexist == true > append(document.body, '

'); - append(document.body, ""); + append(document.body, ""); <#if inviteData?? && inviteData.skill == true && inviteData.consult_skill_fixed == false> document.getElementById("ukefu-im-point-text").onclick=function(){ @@ -419,9 +422,17 @@ document.getElementById("ukefu-point").style.display = "block" ; } -function openAgentChatDialog(url){ - return cskefu.openChatDialogWithURL(url) ; +// 邀请聊天 +function openInviteChatDialog(){ + var url = cskefu.chat + "&agent=" + cskefu.service.agentno + "&isInvite=true"; + return cskefu.openChatDialogWithURL(url); } + +// 技能组或坐席聊天 +function openAgentChatDialog(url){ + return cskefu.openChatDialogWithURL(url); +} + Fingerprint2.get({}, function(components){ var glue = components.map(function (component) { return component.value }) cskefuOnlineUserId = Fingerprint2.x64hash128(glue.join(''), 31) diff --git a/contact-center/app/src/test/java/com/chatopera/cc/acd/ACDComposeContextTest.java b/contact-center/app/src/test/java/com/chatopera/cc/acd/ACDComposeContextTest.java new file mode 100644 index 00000000..f882b6b6 --- /dev/null +++ b/contact-center/app/src/test/java/com/chatopera/cc/acd/ACDComposeContextTest.java @@ -0,0 +1,32 @@ +package com.chatopera.cc.acd; + +import com.chatopera.cc.model.AgentUser; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ACDComposeContextTest extends TestCase { + + private final static Logger logger = LoggerFactory.getLogger(ACDComposeContextTest.class); + + public ACDComposeContextTest(String testName) { + super(testName); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() { + return new TestSuite(ACDComposeContextTest.class); + } + + public void testSetAndGet(){ + ACDComposeContext ctx = new ACDComposeContext(); + ctx.setAgentUser(new AgentUser()); + ctx.getAgentUser().setOrgi("foo"); + assertEquals(ctx.getAgentUser().getOrgi(), "foo"); + } + +} \ No newline at end of file diff --git a/contact-center/app/src/test/java/com/chatopera/cc/basic/MainContextTest.java b/contact-center/app/src/test/java/com/chatopera/cc/basic/MainContextTest.java index 993a1585..890b7c80 100644 --- a/contact-center/app/src/test/java/com/chatopera/cc/basic/MainContextTest.java +++ b/contact-center/app/src/test/java/com/chatopera/cc/basic/MainContextTest.java @@ -43,4 +43,5 @@ public class MainContextTest extends TestCase { assertEquals(MainContext.ChannelType.WEBIM.toString(), "webim"); assertEquals(MainContext.ChannelType.toValue("webim"), MainContext.ChannelType.WEBIM); } + } diff --git a/docker-compose.yml b/docker-compose.yml index f185e183..1b76b0c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: restart: always ports: - "${CC_WEB_PORT:-8035}:8035" - - "${CC_SOCKET_PORT:-8036}:8036" + - "8036:8036" volumes: - ./contact-center/data:/data - ./contact-center/logs:/logs diff --git a/public/plugins/chatbot/classes/ChatbotEventHandler.java b/public/plugins/chatbot/classes/ChatbotEventHandler.java index fc71c48a..970cecf5 100644 --- a/public/plugins/chatbot/classes/ChatbotEventHandler.java +++ b/public/plugins/chatbot/classes/ChatbotEventHandler.java @@ -15,7 +15,6 @@ */ package com.chatopera.cc.plugins.chatbot; -import com.chatopera.cc.acd.AutomaticServiceDist; import com.chatopera.cc.basic.Constants; import com.chatopera.cc.basic.MainContext; import com.chatopera.cc.basic.MainUtils; @@ -209,7 +208,7 @@ public class ChatbotEventHandler { agentUser.setCity(onlineUser.getCity()); agentUser.setProvince(onlineUser.getProvince()); agentUser.setCountry(onlineUser.getCountry()); - AgentService agentService = AutomaticServiceDist.processChatbotService( + AgentService agentService = MainContext.getACDServiceRouter().getAcdChatbotService().processChatbotService( invite != null ? invite.getAiname() : "机器人客服", agentUser, orgi); agentUser.setAgentserviceid(agentService.getId()); @@ -236,7 +235,7 @@ public class ChatbotEventHandler { OnlineUser onlineUser = MainContext.getCache().findOneOnlineUserByUserIdAndOrgi(user, orgi); MainContext.getCache().findOneAgentUserByUserIdAndOrgi(user, orgi).ifPresent(p -> { - AutomaticServiceDist.processChatbotService(null, p, orgi); + MainContext.getACDServiceRouter().getAcdChatbotService().processChatbotService(null, p, orgi); MainContext.getCache().deleteAgentUserByUserIdAndOrgi(user, orgi); MainContext.getCache().deleteOnlineUserByIdAndOrgi(user, orgi); @@ -271,7 +270,9 @@ public class ChatbotEventHandler { String user = client.get("userid"); String sessionid = client.get("session"); String appid = client.get("appid"); - logger.info("[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}, orgi {}", sessionid, aiid, user, data.getType(), appid, orgi); + logger.info( + "[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}, orgi {}", sessionid, aiid, + user, data.getType(), appid, orgi); // ignore event if dataType is not message. if (!StringUtils.equals(data.getType(), Constants.IM_MESSAGE_TYPE_MESSAGE)) { diff --git a/scripts/plugins.install.all.sh b/scripts/plugins.install.all.sh new file mode 100755 index 00000000..29d1ca08 --- /dev/null +++ b/scripts/plugins.install.all.sh @@ -0,0 +1,20 @@ +#! /bin/bash +########################################### +# +########################################### + +# constants +baseDir=$(cd `dirname "$0"`;pwd) +# functions + +# main +[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return +cd $baseDir/.. + +if [ -d ./private/plugins ]; then + ./private/plugins/scripts/install-all.sh +fi + +if [ -d ./public/plugins ]; then + ./public/plugins/scripts/install-all.sh +fi diff --git a/scripts/plugins.uninstall.all.sh b/scripts/plugins.uninstall.all.sh new file mode 100755 index 00000000..ff7b7e50 --- /dev/null +++ b/scripts/plugins.uninstall.all.sh @@ -0,0 +1,20 @@ +#! /bin/bash +########################################### +# +########################################### + +# constants +baseDir=$(cd `dirname "$0"`;pwd) +# functions + +# main +[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return +cd $baseDir/.. + +if [ -d ./private/plugins ]; then + ./private/plugins/scripts/uninstall-all.sh +fi + +if [ -d ./public/plugins ]; then + ./public/plugins/scripts/uninstall-all.sh +fi