diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java index a1c5845..108bb68 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java @@ -35,12 +35,19 @@ public class GroupMessageController { } @GetMapping("/pullOfflineMessage") - @Operation(summary = "拉取离线消息", description = "拉取离线消息,消息将通过webscoket异步推送") + @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送") public Result pullOfflineMessage(@RequestParam Long minId) { groupMessageService.pullOfflineMessage(minId); return ResultUtils.success(); } + @GetMapping(value = "/loadOfflineMessage") + @Operation(summary = "拉取离线消息", description = "拉取离线消息") + public Result> loadOfflineMessage(@RequestParam Long minId) { + return ResultUtils.success(groupMessageService.loadOffineMessage(minId)); + } + + @PutMapping("/readed") @Operation(summary = "消息已读", description = "将群聊中的消息状态置为已读") public Result readedMessage(@RequestParam Long groupId) { diff --git a/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java index 19536a7..4fea71d 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java @@ -35,12 +35,19 @@ public class PrivateMessageController { } @GetMapping("/pullOfflineMessage") - @Operation(summary = "拉取离线消息", description = "拉取离线消息,消息将通过webscoket异步推送") + @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送") public Result pullOfflineMessage(@RequestParam Long minId) { privateMessageService.pullOfflineMessage(minId); return ResultUtils.success(); } + @GetMapping(value = "/loadOfflineMessage") + @Operation(summary = "拉取离线消息", description = "拉取离线消息") + public Result> loadOfflineMessage(@RequestParam Long minId) { + return ResultUtils.success(privateMessageService.loadOfflineMessage(minId)); + } + + @PutMapping("/readed") @Operation(summary = "消息已读", description = "将会话中接收的消息状态置为已读") public Result readedMessage(@RequestParam Long friendId) { diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java index 1a3ed38..5b526b6 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java @@ -31,6 +31,14 @@ public interface GroupMessageService extends IService { */ void pullOfflineMessage(Long minId); + /** + * 拉取离线消息,只能拉取最近1个月的消息 + * + * @param minId 消息起始id + */ + List loadOffineMessage(Long minId); + + /** * 消息已读,同步其他终端,清空未读数量 * diff --git a/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java index 8ebed75..ed2a269 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java @@ -43,6 +43,14 @@ public interface PrivateMessageService extends IService { */ void pullOfflineMessage(Long minId); + /** + * 拉取离线消息,只能拉取最近1个月的消息 + * + * @param minId 消息起始id + */ + List loadOfflineMessage(Long minId); + + /** * 消息已读,将整个会话的消息都置为已读状态 * diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java index 5830074..4714223 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java @@ -80,6 +80,7 @@ public class GroupMessageServiceImpl extends ServiceImpl messages = this.list(wrapper); // 通过群聊对消息进行分组 Map> messageGroupMap = @@ -254,6 +252,83 @@ public class GroupMessageServiceImpl extends ServiceImpl loadOffineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + // 查询用户加入的群组 + List members = groupMemberService.findByUserId(session.getUserId()); + Map groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); + Set groupIds = groupMemberMap.keySet(); + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.gt(GroupMessage::getSendTime, minDate); + wrapper.in(GroupMessage::getGroupId, groupIds); + wrapper.orderByDesc(GroupMessage::getId); + wrapper.last("limit 50000"); + List messages = this.list(wrapper); + // 退群前的消息 + List quitMembers = groupMemberService.findQuitInMonth(session.getUserId()); + for (GroupMember quitMember : quitMembers) { + wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime()); + wrapper.eq(GroupMessage::getGroupId, quitMember.getGroupId()); + wrapper.orderByDesc(GroupMessage::getId); + wrapper.last("limit 1000"); + List groupMessages = this.list(wrapper); + if (!groupMessages.isEmpty()) { + messages.addAll(groupMessages); + groupMemberMap.put(quitMember.getGroupId(), quitMember); + } + } + // 通过群聊对消息进行分组 + Map> messageGroupMap = + messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId)); + List vos = new LinkedList<>(); + for (Map.Entry> entry : messageGroupMap.entrySet()) { + Long groupId = entry.getKey(); + List groupMessages = entry.getValue(); + // 填充消息状态 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString()); + long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString()); + Map maxIdMap = null; + for (GroupMessage m : groupMessages) { + // 排除加群之前的消息 + GroupMember member = groupMemberMap.get(m.getGroupId()); + if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) { + continue; + } + // 排除不需要接收的消息 + List recvIds = CommaTextUtils.asList(m.getRecvIds()); + if (!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())) { + continue; + } + // 组装vo + GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); + // 被@用户列表 + List atIds = CommaTextUtils.asList(m.getAtUserIds()); + vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); + // 填充状态 + vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.PENDING.code()); + // 针对回执消息填充已读人数 + if (m.getReceipt()) { + if (Objects.isNull(maxIdMap)) { + maxIdMap = redisTemplate.opsForHash().entries(key); + } + int count = getReadedUserIds(maxIdMap, m.getId(), m.getSendId()).size(); + vo.setReadedCount(count); + } + vos.add(vo); + } + } + // 排序 + return vos.stream().sorted(Comparator.comparing(GroupMessageVO::getId)).collect(Collectors.toList()); + } + + @Override public void readedMessage(Long groupId) { UserSession session = SessionContext.getSession(); diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java index d0336b8..24c4972 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java @@ -32,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.stream.Collectors; @@ -140,9 +141,8 @@ public class PrivateMessageServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaQuery(); - // 只能拉取最近3个月的消息,移动端只拉取一个月消息 - int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3; - Date minDate = DateUtils.addMonths(new Date(), -months); + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); wrapper.gt(PrivateMessage::getId, minId); wrapper.ge(PrivateMessage::getSendTime, minDate); wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or() @@ -175,6 +175,36 @@ public class PrivateMessageServiceImpl extends ServiceImpl loadOfflineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + // 获取当前用户的消息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + wrapper.gt(PrivateMessage::getId, minId); + wrapper.ge(PrivateMessage::getSendTime, minDate); + wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or() + .eq(PrivateMessage::getRecvId, session.getUserId())); + wrapper.orderByAsc(PrivateMessage::getId); + List messages = this.list(wrapper); + // 更新消息为送达状态 + List messageIds = + messages.stream().filter(m -> m.getStatus().equals(MessageStatus.PENDING.code())).map(PrivateMessage::getId) + .collect(Collectors.toList()); + if (!messageIds.isEmpty()) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(PrivateMessage::getId, messageIds); + updateWrapper.set(PrivateMessage::getStatus, MessageStatus.DELIVERED.code()); + update(updateWrapper); + } + // 转换vo + List vos = messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)) + .collect(Collectors.toList()); + log.info("拉取私聊消息,用户id:{},数量:{},minId:{}", session.getUserId(), messages.size(), minId); + return vos; + } + @Transactional(rollbackFor = Exception.class) @Override public void readedMessage(Long friendId) { diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue index 1bfe6a7..1d83479 100644 --- a/im-uniapp/App.vue +++ b/im-uniapp/App.vue @@ -11,7 +11,9 @@ export default { return { isExit: false, // 是否已退出 audioTip: null, - reconnecting: false // 正在重连标志 + reconnecting: false, // 正在重连标志 + privateMessagesBuffer: [], + groupMessagesBuffer: [], } }, methods: { @@ -36,8 +38,7 @@ export default { this.onReconnectWs(); } else { // 加载离线消息 - this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); - this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId); + this.pullOfflineMessage(); this.configStore.setAppInit(true); } }); @@ -50,11 +51,21 @@ export default { }) this.exit(); } else if (cmd == 3) { - // 私聊消息 - this.handlePrivateMessage(msgInfo); + if (!this.configStore.appInit || this.chatStore.loading) { + // 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序 + this.privateMessagesBuffer.push(msgInfo); + } else { + // 插入私聊消息 + this.handlePrivateMessage(msgInfo); + } } else if (cmd == 4) { - // 群聊消息 - this.handleGroupMessage(msgInfo); + if (!this.configStore.appInit || this.chatStore.loading) { + // 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序 + this.privateMessagesBuffer.push(msgInfo); + } else { + // 插入私聊消息 + this.handlePrivateMessage(msgInfo); + } } else if (cmd == 5) { // 系统消息 this.handleSystemMessage(msgInfo); @@ -84,30 +95,43 @@ export default { this.configStore.clear(); this.userStore.clear(); }, + pullOfflineMessage() { + this.chatStore.setLoading(true); + const promises = []; + promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId)); + promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId)); + Promise.all(promises).then(messages => { + // 处理离线消息 + messages[0].forEach(m => this.handlePrivateMessage(m)); + messages[1].forEach(m => this.handleGroupMessage(m)); + // 处理缓冲区收到的实时消息 + this.privateMessagesBuffer.forEach(m => this.handlePrivateMessage(m)); + this.groupMessagesBuffer.forEach(m => this.handleGroupMessage(m)); + // 清空缓冲区 + this.privateMessagesBuffer = []; + this.groupMessagesBuffer = []; + // 关闭加载离线标记 + this.chatStore.setLoading(false); + // 刷新会话 + this.chatStore.refreshChats(); + }).catch((e) => { + console.log(e) + this.$message.error("拉取离线消息失败"); + this.onExit(); + }) + }, pullPrivateOfflineMessage(minId) { - this.chatStore.setLoadingPrivateMsg(true) - http({ - url: "/message/private/pullOfflineMessage?minId=" + minId, - method: 'GET' - }).catch(() => { - uni.showToast({ - title: "消息拉取失败,请重新登陆", - icon: 'none' - }) - this.exit() + return this.$http({ + url: "/message/private/loadOfflineMessage?minId=" + minId, + method: 'GET', + timeout: 300000 }) }, pullGroupOfflineMessage(minId) { - this.chatStore.setLoadingGroupMsg(true) - http({ - url: "/message/group/pullOfflineMessage?minId=" + minId, - method: 'GET' - }).catch(() => { - uni.showToast({ - title: "消息拉取失败,请重新登陆", - icon: 'none' - }) - this.exit() + return this.$http({ + url: "/message/group/loadOfflineMessage?minId=" + minId, + method: 'GET', + timeout: 300000 }) }, handlePrivateMessage(msg) { @@ -120,11 +144,6 @@ export default { type: 'PRIVATE', targetId: friendId } - // 消息加载标志 - if (msg.type == enums.MESSAGE_TYPE.LOADING) { - this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content)) - return; - } // 消息已读处理,清空已读数量 if (msg.type == enums.MESSAGE_TYPE.READED) { this.chatStore.resetUnreadCount(chatInfo); @@ -204,7 +223,7 @@ export default { this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 this.chatStore.insertMessage(msg, chatInfo); - if (!friend.isDnd && !this.chatStore.isLoading() && + if (!friend.isDnd && !this.chatStore.loading && !msg.selfSend && msgType.isNormal(msg.type) && msg.status != enums.MESSAGE_STATUS.READED) { this.playAudioTip(); @@ -220,11 +239,6 @@ export default { type: 'GROUP', targetId: msg.groupId } - // 消息加载标志 - if (msg.type == enums.MESSAGE_TYPE.LOADING) { - this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content)) - return; - } // 消息已读处理 if (msg.type == enums.MESSAGE_TYPE.READED) { // 我已读对方的消息,清空已读数量 @@ -323,7 +337,7 @@ export default { // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - if (!group.isDnd && !this.chatStore.isLoading() && + if (!group.isDnd && !this.chatStore.loading && !msg.selfSend && msgType.isNormal(msg.type) && msg.status != enums.MESSAGE_STATUS.READED) { this.playAudioTip(); diff --git a/im-uniapp/common/request.js b/im-uniapp/common/request.js index 46775e0..09cfd6b 100644 --- a/im-uniapp/common/request.js +++ b/im-uniapp/common/request.js @@ -17,6 +17,7 @@ const request = (options) => { method: options.method || 'GET', header: header, data: options.data || {}, + timeout: options.timeout || 3000, async success(res) { if (res.data.code == 200) { return resolve(res.data.data) diff --git a/im-uniapp/components/chat-item/chat-item.vue b/im-uniapp/components/chat-item/chat-item.vue index 2b7fc18..ddd79da 100644 --- a/im-uniapp/components/chat-item/chat-item.vue +++ b/im-uniapp/components/chat-item/chat-item.vue @@ -45,7 +45,7 @@ export default { methods: { showChatBox() { // 初始化期间进入会话会导致消息不刷新 - if (!this.configStore.appInit || this.chatStore.isLoading()) { + if (!this.configStore.appInit || this.chatStore.loading) { uni.showToast({ title: "正在初始化页面,请稍后...", icon: 'none' diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 6f05b40..0845cdc 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -325,7 +325,6 @@ export default { let tmpMessage = this.buildTmpMessage(msgInfo); this.chatStore.insertMessage(tmpMessage, chat); this.moveChatToTop(); - this.sendMessageRequest(msgInfo).then((m) => { // 更新消息 tmpMessage = JSON.parse(JSON.stringify(tmpMessage)); @@ -542,7 +541,7 @@ export default { // 删除旧消息 this.chatStore.deleteMessage(msgInfo, chat); // 重新发送 - msgInfo.temId = this.generateId(); + msgInfo.tmpId = this.generateId(); let tmpMessage = this.buildTmpMessage(msgInfo); this.chatStore.insertMessage(tmpMessage, chat); this.moveChatToTop(); diff --git a/im-uniapp/pages/chat/chat.vue b/im-uniapp/pages/chat/chat.vue index f0081ec..9303edc 100644 --- a/im-uniapp/pages/chat/chat.vue +++ b/im-uniapp/pages/chat/chat.vue @@ -111,7 +111,7 @@ export default { return count; }, loading() { - return this.chatStore.isLoading(); + return this.chatStore.loading; }, initializing() { return !this.configStore.appInit; diff --git a/im-uniapp/store/chatStore.js b/im-uniapp/store/chatStore.js index 0d67b83..b7ead33 100644 --- a/im-uniapp/store/chatStore.js +++ b/im-uniapp/store/chatStore.js @@ -3,7 +3,6 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; import useFriendStore from './friendStore.js'; import useGroupStore from './groupStore.js'; import useUserStore from './userStore'; -import useConfigStore from './configStore.js'; let cacheChats = []; export default defineStore('chatStore', { @@ -12,8 +11,7 @@ export default defineStore('chatStore', { chats: [], privateMsgMaxId: 0, groupMsgMaxId: 0, - loadingPrivateMsg: false, - loadingGroupMsg: false + loading: false } }, actions: { @@ -140,7 +138,7 @@ export default defineStore('chatStore', { } }, moveTop(idx) { - if (this.isLoading()) { + if (this.loading) { return; } let chats = this.curChats; @@ -156,20 +154,18 @@ export default defineStore('chatStore', { insertMessage(msgInfo, chatInfo) { // 获取对方id或群id let type = chatInfo.type; - // 完成初始化之前不能修改消息最大id,否则可能导致拉不到离线消息 - if (useConfigStore().appInit) { - // 记录消息的最大id - if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { - this.privateMsgMaxId = msgInfo.id; - } - if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) { - this.groupMsgMaxId = msgInfo.id; - } + // 记录消息的最大id + if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { + this.privateMsgMaxId = msgInfo.id; + } + if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) { + this.groupMsgMaxId = msgInfo.id; } // 如果是已存在消息,则覆盖旧的消息数据 let chat = this.findChat(chatInfo); let message = this.findMessage(chat, msgInfo); if (message) { + console.log("message:",message) Object.assign(message, msgInfo); chat.stored = false; this.saveToStorage(); @@ -218,24 +214,8 @@ export default defineStore('chatStore', { }); chat.lastTimeTip = msgInfo.sendTime; } - // 根据id顺序插入,防止消息乱序 - let insertPos = chat.messages.length; - // 防止 图片、文件 在发送方 显示 在顶端 因为还没存库,id=0 - if (msgInfo.id && msgInfo.id > 0) { - for (let idx in chat.messages) { - if (chat.messages[idx].id && msgInfo.id < chat.messages[idx].id) { - insertPos = idx; - console.log(`消息出现乱序,位置:${chat.messages.length},修正至:${insertPos}`); - break; - } - } - } - if (insertPos == chat.messages.length) { - // 这种赋值效率最高 - chat.messages[insertPos] = msgInfo; - } else { - chat.messages.splice(insertPos, 0, msgInfo); - } + // 插入消息 + chat.messages.push(msgInfo); chat.stored = false; this.saveToStorage(); }, @@ -345,17 +325,8 @@ export default defineStore('chatStore', { this.saveToStorage(); } }, - setLoadingPrivateMsg(loading) { - this.loadingPrivateMsg = loading; - if (!this.isLoading()) { - this.refreshChats() - } - }, - setLoadingGroupMsg(loading) { - this.loadingGroupMsg = loading; - if (!this.isLoading()) { - this.refreshChats() - } + setLoading(loading) { + this.loading = loading; }, setDnd(chatInfo, isDnd) { let chat = this.findChat(chatInfo); @@ -406,7 +377,7 @@ export default defineStore('chatStore', { }, saveToStorage(withColdMessage) { // 加载中不保存,防止卡顿 - if (this.isLoading()) { + if (this.loading) { return; } const userStore = useUserStore(); @@ -493,11 +464,8 @@ export default defineStore('chatStore', { } }, getters: { - isLoading: (state) => () => { - return state.loadingPrivateMsg || state.loadingGroupMsg - }, curChats: (state) => { - if (cacheChats && state.isLoading()) { + if (cacheChats && state.loading) { return cacheChats; } return state.chats; @@ -529,7 +497,10 @@ export default defineStore('chatStore', { if (!chat) { return null; } - for (let idx in chat.messages) { + for (let idx = chat.messages.length - 1; idx >= 0; idx--) { + if (!chat.messages[idx].id && !chat.messages[idx].tmpId) { + continue; + } // 通过id判断 if (msgInfo.id && chat.messages[idx].id == msgInfo.id) { return chat.messages[idx]; @@ -539,7 +510,15 @@ export default defineStore('chatStore', { chat.messages[idx].tmpId == msgInfo.tmpId) { return chat.messages[idx]; } + // 如果id比要查询的消息小,说明没有这条消息 + if (msgInfo.id && msgInfo.id > chat.messages[idx].id) { + break; + } + if (msgInfo.tmpId && msgInfo.tmpId > chat.messages[idx].tmpId) { + break; + } } + return null; } } }); \ No newline at end of file diff --git a/im-web/src/store/chatStore.js b/im-web/src/store/chatStore.js index c0be3f6..d5952dc 100644 --- a/im-web/src/store/chatStore.js +++ b/im-web/src/store/chatStore.js @@ -3,7 +3,6 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js" import useFriendStore from './friendStore.js'; import useGroupStore from './groupStore.js'; import useUserStore from './userStore.js'; -import useConfigStore from './configStore.js'; import localForage from 'localforage'; /** @@ -28,11 +27,10 @@ export default defineStore('chatStore', { state: () => { return { activeChat: null, + chats: [], privateMsgMaxId: 0, groupMsgMaxId: 0, - loadingPrivateMsg: false, - loadingGroupMsg: false, - chats: [] + loading: false } }, actions: { @@ -141,7 +139,7 @@ export default defineStore('chatStore', { }, moveTop(idx) { // 加载中不移动,很耗性能 - if (this.isLoading()) { + if (this.loading) { return; } if (idx > 0) { @@ -156,15 +154,12 @@ export default defineStore('chatStore', { }, insertMessage(msgInfo, chatInfo) { let type = chatInfo.type; - // 完成初始化之前不能修改消息最大id,否则可能导致拉不到离线消息 - if (useConfigStore().appInit) { - // 记录消息的最大id - if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { - this.privateMsgMaxId = msgInfo.id; - } - if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) { - this.groupMsgMaxId = msgInfo.id; - } + // 记录消息的最大id + if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { + this.privateMsgMaxId = msgInfo.id; + } + if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) { + this.groupMsgMaxId = msgInfo.id; } // 如果是已存在消息,则覆盖旧的消息数据 let chat = this.findChat(chatInfo); @@ -218,19 +213,7 @@ export default defineStore('chatStore', { }); chat.lastTimeTip = msgInfo.sendTime; } - // 根据id顺序插入,防止消息乱序 - let insertPos = chat.messages.length; - // 防止 图片、文件 在发送方 显示 在顶端 因为还没存库,id=0 - if (msgInfo.id && msgInfo.id > 0) { - for (let idx in chat.messages) { - if (chat.messages[idx].id && msgInfo.id < chat.messages[idx].id) { - insertPos = idx; - console.log(`消息出现乱序,位置:${chat.messages.length},修正至:${insertPos}`); - break; - } - } - } - chat.messages.splice(insertPos, 0, msgInfo); + chat.messages.push(msgInfo); chat.stored = false; this.saveToStorage(); }, @@ -340,17 +323,8 @@ export default defineStore('chatStore', { this.saveToStorage() } }, - setLoadingPrivateMsg(loading) { - this.loadingPrivateMsg = loading; - if (!this.isLoading()) { - this.refreshChats(); - } - }, - setLoadingGroupMsg(loading) { - this.loadingGroupMsg = loading; - if (!this.isLoading()) { - this.refreshChats(); - } + setLoading(loading) { + this.loading = loading; }, setDnd(chatInfo, isDnd) { let chat = this.findChat(chatInfo); @@ -399,7 +373,7 @@ export default defineStore('chatStore', { }, saveToStorage(withColdMessage) { // 加载中不保存,防止卡顿 - if (this.isLoading()) { + if (this.loading) { return; } const userStore = useUserStore(); @@ -456,6 +430,9 @@ export default defineStore('chatStore', { cacheChats = [] this.chats = []; this.activeChat = null; + this.privateMsgMaxId = 0; + this.groupMsgMaxId = 0; + this.loading = false; }, loadChat() { return new Promise((resolve, reject) => { @@ -511,7 +488,7 @@ export default defineStore('chatStore', { return state.loadingPrivateMsg || state.loadingGroupMsg }, findChats: (state) => () => { - if (cacheChats && state.isLoading()) { + if (cacheChats && state.loading) { return cacheChats; } return state.chats; @@ -545,7 +522,10 @@ export default defineStore('chatStore', { if (!chat) { return null; } - for (let idx in chat.messages) { + for (let idx = chat.messages.length - 1; idx >= 0; idx--) { + if (!chat.messages[idx].id && !chat.messages[idx].tmpId) { + continue; + } // 通过id判断 if (msgInfo.id && chat.messages[idx].id == msgInfo.id) { return chat.messages[idx]; @@ -555,6 +535,13 @@ export default defineStore('chatStore', { chat.messages[idx].tmpId == msgInfo.tmpId) { return chat.messages[idx]; } + // 如果id比要查询的消息小,说明没有这条消息 + if (msgInfo.id && msgInfo.id > chat.messages[idx].id) { + break; + } + if (msgInfo.tmpId && msgInfo.tmpId > chat.messages[idx].tmpId) { + break; + } } } } diff --git a/im-web/src/view/Chat.vue b/im-web/src/view/Chat.vue index 645b1e4..8ce5808 100644 --- a/im-web/src/view/Chat.vue +++ b/im-web/src/view/Chat.vue @@ -89,7 +89,7 @@ export default { }, computed: { loading() { - return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg + return this.chatStore.loading; } } } diff --git a/im-web/src/view/Home.vue b/im-web/src/view/Home.vue index 40d651d..2ae0839 100644 --- a/im-web/src/view/Home.vue +++ b/im-web/src/view/Home.vue @@ -78,7 +78,9 @@ export default { showSettingDialog: false, lastPlayAudioTime: new Date().getTime() - 1000, isFullscreen: true, - reconnecting: false + reconnecting: false, + privateMessagesBuffer: [], + groupMessagesBuffer: [] } }, methods: { @@ -108,8 +110,7 @@ export default { this.onReconnectWs(); } else { // 加载离线消息 - this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); - this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId); + this.pullOfflineMessage(); this.configStore.setAppInit(true); } }); @@ -126,11 +127,21 @@ export default { }); } else if (cmd == 3) { - // 插入私聊消息 - this.handlePrivateMessage(msgInfo); + if (!this.configStore.appInit || this.chatStore.loading) { + // 如果正在拉取离线消息,先放进缓存区,等待消息拉取完成再处理,防止消息乱序 + this.privateMessagesBuffer.push(msgInfo); + } else { + // 插入私聊消息 + this.handlePrivateMessage(msgInfo); + } } else if (cmd == 4) { - // 插入群聊消息 - this.handleGroupMessage(msgInfo); + if (!this.configStore.appInit || this.chatStore.loading) { + // 如果正在拉取离线消息,先放进缓存区,等待消息拉取完成再处理,防止消息乱序 + this.groupMessagesBuffer.push(msgInfo); + } else { + // 插入群聊消息 + this.handleGroupMessage(msgInfo); + } } else if (cmd == 5) { // 处理系统消息 this.handleSystemMessage(msgInfo); @@ -170,8 +181,7 @@ export default { promises.push(this.groupStore.loadGroup()); Promise.all(promises).then(() => { // 加载离线消息 - this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); - this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId); + this.pullOfflineMessage(); this.configStore.setAppInit(true) this.$message.success("重新连接成功"); }).catch(() => { @@ -194,23 +204,42 @@ export default { this.groupStore.clear(); this.chatStore.clear(); this.userStore.clear(); + }, + pullOfflineMessage() { + this.chatStore.setLoading(true); + const promises = []; + promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId)); + promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId)); + Promise.all(promises).then(messages => { + // 处理离线消息 + messages[0].forEach(m => this.handlePrivateMessage(m)); + messages[1].forEach(m => this.handleGroupMessage(m)); + // 处理缓冲区收到的实时消息 + this.privateMessagesBuffer.forEach(m => this.handlePrivateMessage(m)); + this.groupMessagesBuffer.forEach(m => this.handleGroupMessage(m)); + // 清空缓冲区 + this.privateMessagesBuffer = []; + this.groupMessagesBuffer = []; + // 关闭加载离线标记 + this.chatStore.setLoading(false); + // 刷新会话 + this.chatStore.refreshChats(); + }).catch((e) => { + console.log(e) + this.$message.error("拉取离线消息失败"); + this.onExit(); + }) }, pullPrivateOfflineMessage(minId) { - this.chatStore.setLoadingPrivateMsg(true) - this.$http({ - url: "/message/private/pullOfflineMessage?minId=" + minId, + return this.$http({ + url: "/message/private/loadOfflineMessage?minId=" + minId, method: 'GET' - }).catch(() => { - this.chatStore.setLoadingPrivateMsg(false) }) }, pullGroupOfflineMessage(minId) { - this.chatStore.setLoadingGroupMsg(true) - this.$http({ - url: "/message/group/pullOfflineMessage?minId=" + minId, + return this.$http({ + url: "/message/group/loadOfflineMessage?minId=" + minId, method: 'GET' - }).catch(() => { - this.chatStore.setLoadingGroupMsg(false) }) }, handlePrivateMessage(msg) { @@ -223,11 +252,6 @@ export default { type: 'PRIVATE', targetId: friendId } - // 消息加载标志 - if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { - this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content)) - return; - } // 消息已读处理,清空已读数量 if (msg.type == this.$enums.MESSAGE_TYPE.READED) { this.chatStore.resetUnreadCount(chatInfo) @@ -285,7 +309,7 @@ export default { // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - if (!friend.isDnd && !this.chatStore.isLoading() && !msg.selfSend && this.$msgType.isNormal(msg.type) && + if (!friend.isDnd && !this.chatStore.loading && !msg.selfSend && this.$msgType.isNormal(msg.type) && msg.status != this.$enums.MESSAGE_STATUS.READED) { this.playAudioTip(); } @@ -297,11 +321,6 @@ export default { type: 'GROUP', targetId: msg.groupId } - // 消息加载标志 - if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { - this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content)) - return; - } // 消息已读处理 if (msg.type == this.$enums.MESSAGE_TYPE.READED) { // 我已读对方的消息,清空已读数量 @@ -367,7 +386,7 @@ export default { // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - if (!group.isDnd && !this.chatStore.isLoading() && + if (!group.isDnd && !this.chatStore.loading && !msg.selfSend && this.$msgType.isNormal(msg.type) && msg.status != this.$enums.MESSAGE_STATUS.READED) { this.playAudioTip();