增加历史记录功能

This commit is contained in:
2026-04-03 23:50:45 +08:00
parent d4cc7a6b73
commit ae3ac9b819
4 changed files with 199 additions and 5 deletions

View File

@@ -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<Message> 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<Long> userIds = new HashSet<>();
userIds.add(principal.getUserId());
userIds.add(userId);
for (Message msg : messages) {
userIds.add(msg.getFromUserId());
userIds.add(msg.getToUserId());
}
Map<Long, User> userMap = new HashMap<>();
for (Long uid : userIds) {
User u = userService.findById(uid);
if (u != null) userMap.put(uid, u);
}
List<Map<String, Object>> result = messages.stream().map(msg -> {
Map<String, Object> 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<User> users = userService.getAllUsersExcept(principal.getUserId());

View File

@@ -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<Message> getMessages(Long userId1, Long userId2) {
return getMessages(userId1, userId2, DEFAULT_DAYS);
}
public List<Message> 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<Message>()
.and(wrapper -> wrapper
@@ -33,10 +41,61 @@ public class MessageService {
.or()
.eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1)
)
.ge(Message::getCreateTime, since)
.orderByAsc(Message::getCreateTime)
);
}
/**
* 获取历史消息30天之前的
*/
public List<Message> getHistoryMessages(Long userId1, Long userId2, LocalDateTime beforeTime, int limit) {
return messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.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<Message> getRecentMessages(Long userId1, Long userId2, int limit) {
return messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.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<Message> messages = messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.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();
}
/**
* 删除与某个用户的聊天记录(双向,本人会看不到)
*/

View File

@@ -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')

View File

@@ -78,7 +78,12 @@
</div>
<!-- 消息列表 -->
<div class="chat-messages" ref="messagesRef">
<div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
<div class="history-loader" v-if="hasMoreHistory" @click="loadMoreHistory">
<el-button link type="primary" :loading="loadingHistory">
加载更多消息
</el-button>
</div>
<div v-if="loadingMessages" class="messages-loading">
<el-icon class="is-loading"><Loading /></el-icon> 加载中..
</div>
@@ -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() })
</script>
<style scoped>
.chat-container { display: flex; height: 600px; gap: 12px; }
.chat-container { display: flex; height: calc(70vh - 120px); gap: 12px; }
.chat-sidebar { width: 280px; display: flex; flex-direction: column; border-right: 1px solid #e4e7ed; }
.sidebar-tabs { display: flex; border-bottom: 1px solid #e4e7ed; }
.sidebar-tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 13px; }
@@ -782,5 +848,17 @@ onUnmounted(() => { if (unsubscribeWs) unsubscribeWs() })
.chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; gap: 12px; }
.chat-empty p { margin: 0; font-size: 14px; }
.chat-dialog :deep(.el-dialog) { height: 720px; margin: auto !important; top: 50% !important; transform: translateY(-50%) !important; }
.history-loader {
display: flex;
justify-content: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.chat-dialog :deep(.el-dialog) {
height: 70vh;
margin: auto !important;
top: 50% !important;
transform: translateY(-50%) !important;
}
</style>