zoujiajian преди 1 година
родител
ревизия
b520de9344

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfServicerSessionRecordMapper.java

@@ -0,0 +1,18 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.dto.CorpKfServicerDto;
+import com.cyksj.model.entity.CorpKfServicerSessionRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerSessionRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:32
+ */
+public interface CorpKfServicerSessionRecordMapper extends BaseMapper<CorpKfServicerSessionRecord> {
+	List<CorpKfServicerDto> getAvailableServicer(@Param("openKfId") String openKfId);
+}

+ 22 - 0
netflix-dao/src/main/java/com/cyksj/model/dto/CorpKfServicerDto.java

@@ -0,0 +1,22 @@
+package com.cyksj.model.dto;
+
+import com.cyksj.model.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerDto
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:43
+ */
+@Getter
+@Setter
+public class CorpKfServicerDto extends BaseEntity {
+	private String servicerUserid;
+
+	/**
+	 * 接待人数
+	 */
+	private Integer num;
+}

+ 26 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfServicerSessionRecord.java

@@ -0,0 +1,26 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerSessionRecord
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:31
+ */
+@Getter
+@Setter
+public class CorpKfServicerSessionRecord extends BaseEntity{
+	private String openKfId;
+
+	private String servicerUserId;
+
+	private String externalUserid;
+
+	/**
+	 * 接待状态
+	 * 0接待中 1已结束,2.转接
+	 */
+	private Integer serviceState;
+}

+ 1 - 1
netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfServicerResp.java

@@ -27,7 +27,7 @@ public class CorpKfServicerResp {
 	public static class CorpKfServicer{
 		private String userid;
 
-		//	接待人员的接待状态。0:接待中,1:停止接待。
+		//接待人员的接待状态。0:接待中,1:停止接待。
 		private Integer status;
 	}
 }

+ 4 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfMsgListResp.java

@@ -42,6 +42,10 @@ public class WxCpKfMsgListResp {
     private Integer origin;
     @JsonProperty("servicer_userid")
     private String servicerUserId;
+    @JsonProperty("old_servicer_userid")
+    private String oldServicerUserid;
+    @JsonProperty("new_servicer_userid")
+    private String newServicerUserid;
     @JsonProperty("msgtype")
     private String msgType;
     private WxCpKfEventMsg event;

+ 15 - 0
netflix-dao/src/main/resources/mapper/CorpKfServicerSessionRecordMapper.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.cyksj.mapper.corp.kf.CorpKfServicerSessionRecordMapper">
+
+
+    <select id="getAvailableServicer" resultType="com.cyksj.model.dto.CorpKfServicerDto">
+        select servicer_userid,count(1) as num
+        from from corp_kf_servicer_session_reocrd
+        where open_kf_id = #{openKfId}
+          and service_state = 0
+        group by servicer_userid
+        order by num asc
+    </select>
+</mapper>

+ 5 - 0
netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java

@@ -225,4 +225,9 @@ public interface WxCorpOps {
 	CorpKfServicerResp getKfServicerList(String corpId,String openKfId);
 
 	void moveWaitingQueue(String corpId, String openKfId, String externalUserId) throws Exception;
+
+	/**
+	 * 变更微信客服会话状态
+	 */
+	void transCorpKfSessionToServicer(String toUserName, String openKfId, String userid, String externalUserId) throws Exception;
 }

+ 99 - 16
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpServiceImpl.java

@@ -20,10 +20,8 @@ import com.cyksj.config.corp.YHLXWxCorpConfig;
 import com.cyksj.dto.CorpOauth2UserInfo;
 import com.cyksj.mapper.OrderDonMapper;
 import com.cyksj.mapper.corp.*;
-import com.cyksj.mapper.corp.kf.CorpKfAccountFollowMapper;
-import com.cyksj.mapper.corp.kf.CorpKfAccountMapper;
-import com.cyksj.mapper.corp.kf.CorpKfAccountSessionConfigMapper;
-import com.cyksj.mapper.corp.kf.CorpKfWaitingQueueUserMapper;
+import com.cyksj.mapper.corp.kf.*;
+import com.cyksj.model.dto.CorpKfServicerDto;
 import com.cyksj.model.dto.WxCorpEncryptDto;
 import com.cyksj.model.entity.*;
 import com.cyksj.model.kf.CorpKfServicerResp;
@@ -52,10 +50,10 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.PostConstruct;
 import java.io.File;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
@@ -121,6 +119,9 @@ public class CorpServiceImpl implements CorpService {
 	@Autowired
 	private CorpKfWaitingQueueUserMapper corpKfWaitingQueueUserMapper;
 
+	@Autowired
+	private CorpKfServicerSessionRecordMapper corpKfServicerSessionRecordMapper;
+
 	@PostConstruct
 	private void init() {
 		corpMessageRouter = new CorpMessageRouter(this)
@@ -532,13 +533,14 @@ public class CorpServiceImpl implements CorpService {
 			String openKfId = message.getOpenKfId();
 			WxCpKfMsgListResp wxCpKfMsgListResp = wxCorpOps.syncMsg(message.getToUserName(), null, message.getToken(), null, openKfId);
 			List<WxCpKfMsgListResp.WxCpKfMsgItem> msgList = wxCpKfMsgListResp.getMsgList();
-			//欢迎语
+			//进入会话事件
 			enterSessionEvent(message, msgList);
+			//会话更变事件
+			sessionStatusChange(message, msgList);
 			//消息发送失败事件
 			//拒收客户消息变更事件
 			//同步客服聊天记录
 			syncChatMsg(message, msgList);
-			//发送
 		} catch (Exception e) {
 			log.error("企业微信客服 事件消息回调 错误");
 			throw BusinessRuntimeException.getInstance(StringUtil.getErrorMsg(e));
@@ -546,8 +548,40 @@ public class CorpServiceImpl implements CorpService {
 		return null;
 	}
 
+	private void sessionStatusChange(CorpXmlMessage message, List<WxCpKfMsgListResp.WxCpKfMsgItem> msgList) throws Exception {
+		String openKfId = message.getOpenKfId();
+		List<WxCpKfMsgListResp.WxCpKfMsgItem> sessionStatusChanges = msgList.stream()
+				.filter((item) -> item.getEvent() != null && item.getEvent().getEventType().equals("session_status_change")).collect(Collectors.toList());
+		if (CollUtil.isNotEmpty(sessionStatusChanges)) {
+			WxCpKfMsgListResp.WxCpKfMsgItem wxCpKfMsgItem = sessionStatusChanges.get(sessionStatusChanges.size() - 1);
+			String externalUserId = wxCpKfMsgItem.getExternalUserId();
+			String oldServicerUserid = wxCpKfMsgItem.getOldServicerUserid();
+			CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+					.eq(CorpKfServicerSessionRecord::getOpenKfId, openKfId)
+					.eq(CorpKfServicerSessionRecord::getServicerUserId, oldServicerUserid)
+					.eq(CorpKfServicerSessionRecord::getExternalUserid, externalUserId)
+					.last("limit 1"));
+			if (corpKfServicerSessionRecord != null) {
+				corpKfServicerSessionRecord.setServiceState(2);
+				corpKfServicerSessionRecordMapper.updateById(corpKfServicerSessionRecord);
+			}
+			String newServicerUserid = wxCpKfMsgItem.getNewServicerUserid();
+
+			CorpKfServicerSessionRecord corpKfServicerSessionRecord1 = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+					.eq(CorpKfServicerSessionRecord::getOpenKfId, openKfId)
+					.eq(CorpKfServicerSessionRecord::getServicerUserId, newServicerUserid)
+					.eq(CorpKfServicerSessionRecord::getExternalUserid, externalUserId)
+					.eq(CorpKfServicerSessionRecord::getServiceState, 0)
+					.last("limit 1"));
+			if (corpKfServicerSessionRecord1 == null) {
+				//记录新的客服会话
+				transCorpKfSessionToServicer(message.getToUserName(), openKfId, newServicerUserid, externalUserId);
+			}
+		}
+	}
+
 	/**
-	 * 微信客服欢迎语
+	 * enter_session 会话事件
 	 */
 	public void enterSessionEvent(CorpXmlMessage message, List<WxCpKfMsgListResp.WxCpKfMsgItem> msgList) throws Exception {
 		String openKfId = message.getOpenKfId();
@@ -564,17 +598,16 @@ public class CorpServiceImpl implements CorpService {
 		CorpKfAccountSessionConfig corpKfAccountSessionConfig = corpKfAccountSessionConfigMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountSessionConfig.class)
 				.eq(CorpKfAccountSessionConfig::getOpenKfId, openKfId)
 				.last("limit 1"));
-		//欢迎语
 		if (enterSessionList.size() > 0) {
 			WxCpKfMsgListResp.WxCpKfMsgItem wxCpKfMsgItem = enterSessionList.get(enterSessionList.size() - 1);
 			WxCpKfEventMsg event = wxCpKfMsgItem.getEvent();
 			String welcomeCode = event.getWelcomeCode();
-			//如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
-			//可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。
 			if (corpKfAccountSessionConfig == null) {
 				log.info("openKfId:{}未设置会话设置", openKfId);
 				return;
 			}
+			//如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
+			//可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。
 			if (StrUtil.isNotEmpty(welcomeCode)) {
 				WxCorpKfMsgSendRequest wxCpKfMsgSendRequest = new WxCorpKfMsgSendRequest();
 				wxCpKfMsgSendRequest.setMsgType("text");
@@ -590,6 +623,15 @@ public class CorpServiceImpl implements CorpService {
 				wxCpKfMsgSendRequest.setOpenKfid(event.getOpenKfid());
 				wxCorpOps.sendMsgOnEvent(message.getToUserName(), wxCpKfMsgSendRequest);
 			} else {
+				//用户是否已有会话
+				Integer selectCount = corpKfServicerSessionRecordMapper.selectCount(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+						.eq(CorpKfServicerSessionRecord::getOpenKfId, openKfId)
+						.eq(CorpKfServicerSessionRecord::getExternalUserid, event.getExternalUserId())
+						.eq(CorpKfServicerSessionRecord::getServiceState, 0));
+				if (selectCount > 0) {
+					log.info("external_userid:{}已有未结束的会话,无需分配", event.getExternalUserId());
+					return;
+				}
 				//配置接待人员
 				//是否有空闲人员
 				int hour = DateTime.now().hour(true);
@@ -600,7 +642,15 @@ public class CorpServiceImpl implements CorpService {
 				//获取接待人员列表
 				CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(message.getToUserName(), openKfId);
 				List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
-				List<CorpKfServicerResp.CorpKfServicer> corpKfServicers = servicerList.stream().filter(servicer -> servicer.getUserid() != null && servicer.getStatus() == 0).collect(Collectors.toList());
+				//目前值班客服id
+				List<String> corpKfServiceIds = new ArrayList<>();
+				List<CorpKfServicerResp.CorpKfServicer> corpKfServicers = servicerList.stream().filter(servicer ->{
+					if (servicer.getUserid() != null && servicer.getStatus() == 0) {
+						corpKfServiceIds.add(servicer.getUserid());
+						return true;
+					}
+					return false;
+				}).collect(Collectors.toList());
 				if (CollUtil.isEmpty(corpKfServicers)) {
 					//非值班 移入等待池并发送非值班提示消息
 					moveWaitingQueue(message.getToUserName(), openKfId, event.getExternalUserId());
@@ -611,14 +661,47 @@ public class CorpServiceImpl implements CorpService {
 					}
 					return;
 				}
-				//未有可接待的客服 移入等待池并发送进入等待池消息
-				moveWaitingQueue(message.getToUserName(), openKfId, event.getExternalUserId());
-				String queueTips = corpKfAccountSessionConfig.getQueueTips();
-				sendCorpKfTextTips(message.getToUserName(), event.getExternalUserId(), openKfId, queueTips);
+				//是否有空闲接待人员
+				List<CorpKfServicerDto> corpKfServicerDtos = corpKfServicerSessionRecordMapper.getAvailableServicer(openKfId);
+				if (CollUtil.isEmpty(corpKfServicerDtos)) {
+					//暂无分配会话
+					//随机分配一个客服
+					CorpKfServicerResp.CorpKfServicer corpKfServicer = corpKfServicers.get(RandomUtil.randomInt(corpKfServicers.size()));
+					//变更会话状态
+					transCorpKfSessionToServicer(message.getToUserName(), openKfId, corpKfServicer.getUserid(), event.getExternalUserId());
+				}
+				//筛选符合条件的客服
+				//默认接入最少的优先分配 接待人最少的在集合前面
+				//最多接待人数
+				Integer maxNum = corpKfAccountSessionConfig.getMaxNum();
+				List<CorpKfServicerDto> filterKfCs = corpKfServicerDtos.stream().filter(kfCs -> corpKfServiceIds.contains(kfCs.getServicerUserid()) && kfCs.getNum() < maxNum).collect(Collectors.toList());
+				if (CollUtil.isEmpty(filterKfCs)) {
+					//未有可接待的客服 移入等待池并发送进入等待池消息
+					moveWaitingQueue(message.getToUserName(), openKfId, event.getExternalUserId());
+					String queueTips = corpKfAccountSessionConfig.getQueueTips();
+					sendCorpKfTextTips(message.getToUserName(), event.getExternalUserId(), openKfId, queueTips);
+					return;
+				}
+				//选择第一个分配接待
+				CorpKfServicerDto corpKfServicerDto = filterKfCs.get(0);
+				//变更会话状态
+				transCorpKfSessionToServicer(message.getToUserName(), openKfId, corpKfServicerDto.getServicerUserid(), event.getExternalUserId());
 			}
 		}
 	}
 
+	private void transCorpKfSessionToServicer(String corpId, String openKfId, String userid, String externalUserId) throws Exception {
+		//记录接待人员会话
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = new CorpKfServicerSessionRecord();
+		corpKfServicerSessionRecord.setOpenKfId(openKfId);
+		corpKfServicerSessionRecord.setServiceState(0);
+		corpKfServicerSessionRecord.setServicerUserId(userid);
+		corpKfServicerSessionRecord.setExternalUserid(externalUserId);
+		corpKfServicerSessionRecordMapper.insert(corpKfServicerSessionRecord);
+		//调用微信客服接口 变更会话状态
+		wxCorpOps.transCorpKfSessionToServicer(corpId, openKfId, userid, externalUserId);
+	}
+
 	/**
 	 * 发送微信客服会话文本消息
 	 */

+ 26 - 0
netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java

@@ -988,4 +988,30 @@ public class WxCorpOpsImpl implements WxCorpOps {
 			throw BusinessRuntimeException.getInstance("移入等待池响应消息: " + resp.getErrmsg());
 		}
 	}
+
+	@Override
+	public void transCorpKfSessionToServicer(String corpId, String openKfId, String userid, String externalUserId) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=" + getKfAccessToken(corpId);
+		JSONObject request = new JSONObject();
+		request.putOpt("open_kfid", openKfId);
+		request.putOpt("external_userid", externalUserId);
+		//待接入池排队中
+		request.putOpt("service_state", 3);
+		request.putOpt("servicer_userid", userid);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(request), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("客服接入会话响应消息: " + res.body());
+		}
+
+		CorpKfSessionResp resp = Jsons.parseObject(res.body(), CorpKfSessionResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("客服接入会话响应消息: " + resp.getErrmsg());
+		}
+	}
 }