|
@@ -0,0 +1,316 @@
|
|
|
|
|
+package com.zksy.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.zksy.common.exception.BusinessException;
|
|
|
|
|
+import com.zksy.domain.ChatMessage;
|
|
|
|
|
+import com.zksy.domain.ChatSession;
|
|
|
|
|
+import com.zksy.domain.dto.ChatMessageDto;
|
|
|
|
|
+import com.zksy.domain.vo.ChatSeesionVo;
|
|
|
|
|
+import com.zksy.mapper.ChatMapper;
|
|
|
|
|
+import com.zksy.mapper.ChatMessageMapper;
|
|
|
|
|
+import com.zksy.mapper.ChatSessionMapper;
|
|
|
|
|
+import com.zksy.service.ChatService;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+
|
|
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class ChatServiceImpl implements ChatService {
|
|
|
|
|
+
|
|
|
|
|
+ private final ChatMessageMapper chatMessageMapper;
|
|
|
|
|
+ private final ChatSessionMapper chatSessionMapper;
|
|
|
|
|
+ private final RedisTemplate<Object, Object> redisTemplate;
|
|
|
|
|
+ private final ChatMapper chatMapper;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void clearOnlineStatusOnStartup() {
|
|
|
|
|
+ redisTemplate.delete("online:users");
|
|
|
|
|
+ redisTemplate.delete("online:admins");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onUserLogin(String userId) {
|
|
|
|
|
+ System.out.println("业务逻辑:记录用户上线日志 -> " + userId);
|
|
|
|
|
+ redisTemplate.opsForSet().add("online:users", userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onAdminLogin(String adminId) {
|
|
|
|
|
+ System.out.println("业务逻辑:记录客服上线日志 -> " + adminId);
|
|
|
|
|
+ redisTemplate.opsForSet().add("online:admins", adminId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Long> onUserMessage(ChatMessageDto chatMsg) {
|
|
|
|
|
+ Long sessionId = getOrCreateSession(chatMsg.getFrom(), chatMsg.getTo());
|
|
|
|
|
+ Long messageId = saveMessage(sessionId, chatMsg, "sent"); // 用户发出消息,默认 sent
|
|
|
|
|
+ return Map.of("sessionId", sessionId, "messageId", messageId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Long> onAdminMessage(ChatMessageDto chatMsg) {
|
|
|
|
|
+ Long sessionId = getOrCreateSession(chatMsg.getTo(), chatMsg.getFrom());
|
|
|
|
|
+ Long messageId = saveMessage(sessionId, chatMsg, "sent");// 客服发出消息,默认 sent
|
|
|
|
|
+ return Map.of("sessionId", sessionId, "messageId", messageId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onUserOffline(String userId) {
|
|
|
|
|
+ System.out.println("业务逻辑:标记用户离线 -> " + userId);
|
|
|
|
|
+ redisTemplate.opsForSet().remove("online:users", userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onAdminOffline(String adminId) {
|
|
|
|
|
+ System.out.println("业务逻辑:标记客服离线 -> " + adminId);
|
|
|
|
|
+ redisTemplate.opsForSet().remove("online:admins", adminId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onHeartbeat(ChatMessageDto chatMsg) {
|
|
|
|
|
+ if (!chatMsg.getRole().equals("admin")) {
|
|
|
|
|
+ redisTemplate.opsForSet().add("online:users", chatMsg.getFrom());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ redisTemplate.opsForSet().add("online:admins", chatMsg.getFrom());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onNoAvailableAdmin(ChatMessageDto chatMsg) {
|
|
|
|
|
+ chatMsg.setTo("robot");
|
|
|
|
|
+ System.out.println("业务逻辑:用户无客服可联系 -> " + chatMsg.getFrom());
|
|
|
|
|
+ // 可以在这里生成一个临时消息
|
|
|
|
|
+ saveMessage(0L, chatMsg, "sent"); // 系统消息
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onMessageSendFailure(ChatMessageDto chatMsg) {
|
|
|
|
|
+ Long sessionId = getOrCreateSession(chatMsg.getFrom(), chatMsg.getTo());
|
|
|
|
|
+ saveMessage(sessionId, chatMsg, "failed"); // 标记失败
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onReadMessage(ChatMessageDto chatMsg) {
|
|
|
|
|
+ if (chatMsg.getMessageIds() == null || chatMsg.getMessageIds().length == 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 批量更新消息状态为已读
|
|
|
|
|
+ LambdaUpdateWrapper<ChatMessage> updateWrapper = new LambdaUpdateWrapper<ChatMessage>()
|
|
|
|
|
+ .in(ChatMessage::getId, Arrays.asList(chatMsg.getMessageIds()))
|
|
|
|
|
+ .ne(ChatMessage::getStatus, "read"); // 只更新状态不是已读的消息
|
|
|
|
|
+
|
|
|
|
|
+ ChatMessage updateMessage = new ChatMessage();
|
|
|
|
|
+ updateMessage.setStatus("read");
|
|
|
|
|
+
|
|
|
|
|
+ int updatedCount = chatMessageMapper.update(updateMessage, updateWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (updatedCount > 0) {
|
|
|
|
|
+ System.out.println("成功标记 " + updatedCount + " 条消息为已读状态");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ System.err.println("标记消息已读失败: " + e.getMessage());
|
|
|
|
|
+ // 可以选择抛出异常或记录日志
|
|
|
|
|
+ throw new BusinessException("标记消息已读失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean isClientService(String from) {
|
|
|
|
|
+ var clients = chatMapper.getAllClientService();
|
|
|
|
|
+ return clients.contains(from);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String getToken(HttpServletRequest request) {
|
|
|
|
|
+ //获取ip地址 计数
|
|
|
|
|
+ String ip = request.getRemoteAddr();
|
|
|
|
|
+ Integer count = (Integer) redisTemplate.opsForValue().get(ip);
|
|
|
|
|
+
|
|
|
|
|
+ if (count != null && count >= 999) {
|
|
|
|
|
+ throw new BusinessException("您已访问次数过多,稍后再试");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (count == null) {
|
|
|
|
|
+ count = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ redisTemplate.opsForValue().set(ip, count + 1, 1, TimeUnit.HOURS);
|
|
|
|
|
+
|
|
|
|
|
+ String token = java.util.UUID.randomUUID().toString().replaceAll("-", "");
|
|
|
|
|
+ redisTemplate.opsForValue().set("ws:token:" + token, 1, 30, TimeUnit.SECONDS);
|
|
|
|
|
+ return token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<ChatSeesionVo> getSessionListByAdminId(String adminId) {
|
|
|
|
|
+ // 1. 先获取所有会话
|
|
|
|
|
+ var list = chatSessionMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<ChatSession>()
|
|
|
|
|
+ .eq(ChatSession::getAssignedAdminId, adminId));
|
|
|
|
|
+
|
|
|
|
|
+ if (list.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 批量获取所有会话ID
|
|
|
|
|
+ List<Long> sessionIds = list.stream()
|
|
|
|
|
+ .map(ChatSession::getId)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 批量查询未读消息数量 - 按会话分组统计
|
|
|
|
|
+ Map<Long, Long> unreadCountMap = chatMessageMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<ChatMessage>()
|
|
|
|
|
+ .in(ChatMessage::getSessionId, sessionIds)
|
|
|
|
|
+ .ne(ChatMessage::getStatus, "read"))
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ ChatMessage::getSessionId,
|
|
|
|
|
+ Collectors.counting()
|
|
|
|
|
+ ));
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 批量查询最后一条消息 - 按会话获取最新消息
|
|
|
|
|
+ Map<Long, ChatMessage> lastMessageMap = chatMessageMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<ChatMessage>()
|
|
|
|
|
+ .in(ChatMessage::getSessionId, sessionIds)
|
|
|
|
|
+ .orderByDesc(ChatMessage::getCreatedAt))
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .collect(Collectors.toMap(
|
|
|
|
|
+ ChatMessage::getSessionId,
|
|
|
|
|
+ message -> message,
|
|
|
|
|
+ (existing, replacement) -> existing // 保留第一个(最新的)
|
|
|
|
|
+ ));
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 构建结果
|
|
|
|
|
+ return list.stream().map(session -> {
|
|
|
|
|
+ var userId = session.getUserId();
|
|
|
|
|
+ var userOnline = redisTemplate.opsForSet().isMember("online:users", userId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取未读数量
|
|
|
|
|
+ Long unreadCount = unreadCountMap.getOrDefault(session.getId(), 0L);
|
|
|
|
|
+ var unRead = unreadCount > 100 ? 100 : unreadCount.intValue();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取最后一条消息
|
|
|
|
|
+ ChatMessage lastMessageEntity = lastMessageMap.get(session.getId());
|
|
|
|
|
+ String lastMessage = "";
|
|
|
|
|
+ String lastMessageTime = "";
|
|
|
|
|
+
|
|
|
|
|
+ if (lastMessageEntity != null) {
|
|
|
|
|
+ lastMessage = lastMessageEntity.getContent();
|
|
|
|
|
+ if (lastMessageEntity.getCreatedAt() != null) {
|
|
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
|
|
|
|
+ lastMessageTime = lastMessageEntity.getCreatedAt().format(formatter);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new ChatSeesionVo(
|
|
|
|
|
+ session.getId(),
|
|
|
|
|
+ session.getUserId(),
|
|
|
|
|
+ userOnline,
|
|
|
|
|
+ false,
|
|
|
|
|
+ lastMessage,
|
|
|
|
|
+ lastMessageTime,
|
|
|
|
|
+ unRead
|
|
|
|
|
+ );
|
|
|
|
|
+ }).collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public HashMap<String, Object> getChatMessageListBySessionId(Long sessionId, Integer pageNum, Integer pageSize) {
|
|
|
|
|
+ //分页根据时间倒序
|
|
|
|
|
+ Page<ChatMessage> page = chatMessageMapper.selectPage(
|
|
|
|
|
+ new Page<>(pageNum, pageSize),
|
|
|
|
|
+ new LambdaQueryWrapper<ChatMessage>()
|
|
|
|
|
+ .eq(ChatMessage::getSessionId, sessionId)
|
|
|
|
|
+ .orderByDesc(ChatMessage::getCreatedAt)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ HashMap<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("messages", page.getRecords());
|
|
|
|
|
+ result.put("total", page.getTotal());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String readMessage(Long messageId) {
|
|
|
|
|
+ // 检查消息是否存在
|
|
|
|
|
+ ChatMessage message = chatMessageMapper.selectById(messageId);
|
|
|
|
|
+ if (message == null) {
|
|
|
|
|
+ throw new BusinessException("消息不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ chatMessageMapper.update(
|
|
|
|
|
+ new ChatMessage(),
|
|
|
|
|
+ new LambdaUpdateWrapper<ChatMessage>()
|
|
|
|
|
+ .eq(ChatMessage::getId, messageId)
|
|
|
|
|
+ .set(ChatMessage::getStatus, "read")
|
|
|
|
|
+ );
|
|
|
|
|
+ return "消息已标记为已读";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Page<ChatMessage> getRobotMessage(Integer pageNum, Integer pageSize) {
|
|
|
|
|
+ return chatMessageMapper.selectPage(
|
|
|
|
|
+ new Page<>(pageNum, pageSize),
|
|
|
|
|
+ new LambdaQueryWrapper<ChatMessage>()
|
|
|
|
|
+ .eq(ChatMessage::getToId, "robot")
|
|
|
|
|
+ .ne(ChatMessage::getStatus, "read")
|
|
|
|
|
+ .orderByDesc(ChatMessage::getCreatedAt)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取会话,没有则新建
|
|
|
|
|
+ */
|
|
|
|
|
+ private Long getOrCreateSession(String userId, String adminId) {
|
|
|
|
|
+ ChatSession session = chatSessionMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<ChatSession>()
|
|
|
|
|
+ .eq(ChatSession::getUserId, userId)
|
|
|
|
|
+ .eq(ChatSession::getAssignedAdminId, adminId)
|
|
|
|
|
+ .last("LIMIT 1")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (session == null) {
|
|
|
|
|
+ session = new ChatSession();
|
|
|
|
|
+ session.setUserId(userId);
|
|
|
|
|
+ session.setAssignedAdminId(adminId);
|
|
|
|
|
+ session.setStatus("active");
|
|
|
|
|
+ session.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
+ session.setUpdatedAt(LocalDateTime.now());
|
|
|
|
|
+ chatSessionMapper.insert(session);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ session.setUpdatedAt(LocalDateTime.now());
|
|
|
|
|
+ chatSessionMapper.updateById(session);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return session.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存消息
|
|
|
|
|
+ */
|
|
|
|
|
+ private Long saveMessage(Long sessionId, ChatMessageDto chatMsg, String status) {
|
|
|
|
|
+ ChatMessage msg = new ChatMessage();
|
|
|
|
|
+ msg.setSessionId(sessionId);
|
|
|
|
|
+ msg.setFromId(chatMsg.getFrom());
|
|
|
|
|
+ msg.setFromRole(chatMsg.getRole());
|
|
|
|
|
+ msg.setToId(chatMsg.getTo());
|
|
|
|
|
+ msg.setMsgType(chatMsg.getType());
|
|
|
|
|
+ msg.setContent(chatMsg.getContent());
|
|
|
|
|
+ msg.setStatus(status);
|
|
|
|
|
+ msg.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
+ chatMessageMapper.insert(msg);
|
|
|
|
|
+ return msg.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|