diff --git a/src/main/java/com/filesystem/controller/MessageController.java b/src/main/java/com/filesystem/controller/MessageController.java index 93743d8..812a843 100644 --- a/src/main/java/com/filesystem/controller/MessageController.java +++ b/src/main/java/com/filesystem/controller/MessageController.java @@ -174,6 +174,61 @@ public class MessageController { return ResponseEntity.ok(Map.of("data", result)); } + @GetMapping("/history") + public ResponseEntity getHistoryMessages( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam Long userId, + @RequestParam(required = false) String beforeTime, + @RequestParam(defaultValue = "20") int limit) { + + List messages; + if (beforeTime != null && !beforeTime.isEmpty()) { + LocalDateTime before = LocalDateTime.parse(beforeTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + messages = messageService.getHistoryMessages(principal.getUserId(), userId, before, limit); + } else { + messages = messageService.getRecentMessages(principal.getUserId(), userId, limit); + } + + Set userIds = new HashSet<>(); + userIds.add(principal.getUserId()); + userIds.add(userId); + for (Message msg : messages) { + userIds.add(msg.getFromUserId()); + userIds.add(msg.getToUserId()); + } + Map userMap = new HashMap<>(); + for (Long uid : userIds) { + User u = userService.findById(uid); + if (u != null) userMap.put(uid, u); + } + + List> result = messages.stream().map(msg -> { + Map m = new HashMap<>(); + m.put("id", msg.getId()); + m.put("fromUserId", msg.getFromUserId()); + m.put("toUserId", msg.getToUserId()); + m.put("content", msg.getContent()); + m.put("type", msg.getType()); + m.put("fileName", msg.getFileName()); + m.put("fileSize", msg.getFileSize()); + m.put("isRead", msg.getIsRead()); + m.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : ""); + + User fromUser = userMap.get(msg.getFromUserId()); + if (fromUser != null) { + m.put("fromUsername", fromUser.getUsername()); + m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); + m.put("fromAvatar", fromUser.getAvatar()); + m.put("fromSignature", fromUser.getSignature()); + m.put("fromPhone", fromUser.getPhone()); + m.put("fromEmail", fromUser.getEmail()); + } + return m; + }).collect(Collectors.toList()); + + return ResponseEntity.ok(Map.of("data", result)); + } + @GetMapping("/users") public ResponseEntity getUsers(@AuthenticationPrincipal UserPrincipal principal) { List users = userService.getAllUsersExcept(principal.getUserId()); diff --git a/src/main/java/com/filesystem/service/MessageService.java b/src/main/java/com/filesystem/service/MessageService.java index 93b4c5d..964b561 100644 --- a/src/main/java/com/filesystem/service/MessageService.java +++ b/src/main/java/com/filesystem/service/MessageService.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -21,11 +22,18 @@ public class MessageService { private StringRedisTemplate redisTemplate; private static final String CONV_DELETED_KEY = "msg:del:"; + private static final int DEFAULT_DAYS = 30; // 默认显示最近30天 public List getMessages(Long userId1, Long userId2) { + return getMessages(userId1, userId2, DEFAULT_DAYS); + } + + public List getMessages(Long userId1, Long userId2, int days) { String convKey = buildConvKey(userId1, userId2); String deletedKey = CONV_DELETED_KEY + userId1 + ":" + convKey; + LocalDateTime since = LocalDateTime.now().minusDays(days); + return messageMapper.selectList( new LambdaQueryWrapper() .and(wrapper -> wrapper @@ -33,9 +41,60 @@ public class MessageService { .or() .eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1) ) + .ge(Message::getCreateTime, since) .orderByAsc(Message::getCreateTime) ); } + + /** + * 获取历史消息(30天之前的) + */ + public List getHistoryMessages(Long userId1, Long userId2, LocalDateTime beforeTime, int limit) { + return messageMapper.selectList( + new LambdaQueryWrapper() + .and(wrapper -> wrapper + .eq(Message::getFromUserId, userId1).eq(Message::getToUserId, userId2) + .or() + .eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1) + ) + .lt(Message::getCreateTime, beforeTime) + .orderByDesc(Message::getCreateTime) + .last("LIMIT " + limit) + ); + } + + /** + * 获取最近的消息(分页加载) + */ + public List getRecentMessages(Long userId1, Long userId2, int limit) { + return messageMapper.selectList( + new LambdaQueryWrapper() + .and(wrapper -> wrapper + .eq(Message::getFromUserId, userId1).eq(Message::getToUserId, userId2) + .or() + .eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1) + ) + .orderByDesc(Message::getCreateTime) + .last("LIMIT " + limit) + ); + } + + /** + * 获取最早的30天消息,用于判断是否有更早的消息 + */ + public LocalDateTime getEarliestMessageTime(Long userId1, Long userId2) { + List messages = messageMapper.selectList( + new LambdaQueryWrapper() + .and(wrapper -> wrapper + .eq(Message::getFromUserId, userId1).eq(Message::getToUserId, userId2) + .or() + .eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1) + ) + .orderByAsc(Message::getCreateTime) + .last("LIMIT 1") + ); + return messages.isEmpty() ? null : messages.get(0).getCreateTime(); + } /** * 删除与某个用户的聊天记录(双向,本人会看不到) diff --git a/web-vue/src/api/message.js b/web-vue/src/api/message.js index d5fdf5b..7adae98 100644 --- a/web-vue/src/api/message.js +++ b/web-vue/src/api/message.js @@ -2,6 +2,8 @@ export const getMessages = (params) => request.get('/messages', { params }) +export const getHistoryMessages = (params) => request.get('/messages/history', { params }) + export const sendMessage = (data) => request.post('/messages', data) export const getUnreadCount = () => request.get('/messages/unreadCount') diff --git a/web-vue/src/components/ChatDialog.vue b/web-vue/src/components/ChatDialog.vue index 32bc9d0..135bdc6 100644 --- a/web-vue/src/components/ChatDialog.vue +++ b/web-vue/src/components/ChatDialog.vue @@ -78,7 +78,12 @@ -
+
+
+ + 加载更多消息 + +
加载中..
@@ -227,7 +232,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ElMessage } from 'element-plus' import { ChatLineRound, User, Search, Loading, Picture, Paperclip, Download, ChatDotRound, Promotion, Delete } from '@element-plus/icons-vue' import { useUserStore } from '@/store/user' -import { getUsers, getMessages, sendMessage as sendMessageApi, uploadChatFile, getUnreadList, deleteConversation } from '@/api/message' +import { getUsers, getMessages, getHistoryMessages, sendMessage as sendMessageApi, uploadChatFile, getUnreadList, deleteConversation } from '@/api/message' import { chatService } from '@/services/chat' const props = defineProps({ modelValue: Boolean }) @@ -250,6 +255,8 @@ const currentUserNickname = ref('') const currentContact = ref(null) const inputText = ref('') const loadingMessages = ref(false) +const loadingHistory = ref(false) +const hasMoreHistory = ref(false) const emojiVisible = ref(false) const messagesRef = ref(null) const imageInputRef = ref(null) @@ -380,10 +387,11 @@ const loadUnreadChats = async () => { const selectContact = async (contact) => { currentContact.value = contact contact.unread = 0 + hasMoreHistory.value = false loadingMessages.value = true try { const res = await getMessages({ userId: contact.id }) - messages.value[contact.id] = (res.data || []).map(msg => { + const list = (res.data || []).map(msg => { const isSelf = String(msg.fromUserId) === String(userStore.userId) return { ...msg, @@ -393,6 +401,9 @@ const selectContact = async (contact) => { fromColor: colors[Math.abs(Number(isSelf ? userStore.userId : msg.fromUserId) % colors.length)] } }) + messages.value[contact.id] = list + // 如果消息条数 >= 20,可能还有更早的历史 + hasMoreHistory.value = list.length >= 20 updateRecentChat(contact, '') } catch (e) { ElMessage.error('加载消息失败') @@ -402,6 +413,51 @@ const selectContact = async (contact) => { } } +const loadMoreHistory = async () => { + if (!currentContact.value || loadingHistory.value || !hasMoreHistory.value) return + const list = messages.value[currentContact.value.id] + if (!list || list.length === 0) return + + loadingHistory.value = true + // 记录当前滚动高度,加载后恢复 + const prevScrollHeight = messagesRef.value?.scrollHeight || 0 + try { + const firstMsg = list[0] + const beforeTime = firstMsg.createTime || firstMsg.id + const res = await getHistoryMessages({ userId: currentContact.value.id, beforeTime, limit: 20 }) + const historyList = (res.data || []).map(msg => { + const isSelf = String(msg.fromUserId) === String(userStore.userId) + return { + ...msg, + isSelf, + sending: false, + failed: false, + fromColor: colors[Math.abs(Number(isSelf ? userStore.userId : msg.fromUserId) % colors.length)] + } + }) + + if (historyList.length === 0) { + hasMoreHistory.value = false + } else { + // 插入到消息列表前面 + messages.value[currentContact.value.id] = [...historyList, ...list] + // 如果返回条数少于 limit,没有更多了 + hasMoreHistory.value = historyList.length >= 20 + // 恢复滚动位置 + nextTick(() => { + if (messagesRef.value) { + const newScrollHeight = messagesRef.value.scrollHeight + messagesRef.value.scrollTop = newScrollHeight - prevScrollHeight + } + }) + } + } catch (e) { + ElMessage.error('加载历史消息失败') + } finally { + loadingHistory.value = false + } +} + const updateRecentChat = (contact, lastMsg) => { let chat = recentChats.value.find(c => c.id === contact.id) if (!chat) { @@ -600,6 +656,16 @@ const scrollToBottom = () => { }) } +// 滚动到顶部时加载更多历史消息 +const handleScroll = () => { + if (!messagesRef.value || !currentContact.value) return + const { scrollTop } = messagesRef.value + // 滚动到顶部 50px 以内时触发 + if (scrollTop < 50 && hasMoreHistory.value && !loadingHistory.value) { + loadMoreHistory() + } +} + // ======== WebSocket ======== const onDialogOpen = () => { @@ -683,7 +749,7 @@ onUnmounted(() => { if (unsubscribeWs) unsubscribeWs() })