diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java index 39d4990..e9c0856 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java @@ -25,8 +25,8 @@ public class FileController { @Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url") @PostMapping("/image/upload") public Result uploadImage(@RequestParam("file") MultipartFile file, - @RequestParam(defaultValue = "true") Boolean isPermanent) { - return ResultUtils.success(fileService.uploadImage(file,isPermanent)); + @RequestParam(defaultValue = "true") Boolean isPermanent, @RequestParam(defaultValue = "50") Long thumbSize) { + return ResultUtils.success(fileService.uploadImage(file, isPermanent,thumbSize)); } @Operation(summary = "上传文件", description = "上传文件,上传后返回文件url") 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/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/FileService.java index bc32e19..2540b9e 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/FileService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/FileService.java @@ -9,7 +9,7 @@ public interface FileService extends IService { String uploadFile(MultipartFile file); - UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent); + UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent,Long thumbSize); } 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/FileServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java index 150bc2c..761f2dc 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java @@ -92,7 +92,7 @@ public class FileServiceImpl extends ServiceImpl imple @Transactional @Override - public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent) { + public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent,Long thumbSize) { try { Long userId = SessionContext.getSession().getUserId(); // 大小校验 @@ -129,9 +129,9 @@ public class FileServiceImpl extends ServiceImpl imple throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); } vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); - if (file.getSize() > 50 * 1024) { + if (file.getSize() > thumbSize * 1024) { // 大于50K的文件需上传缩略图 - byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); + byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), thumbSize); String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), file.getOriginalFilename(), imageByte, file.getContentType()); if (StringUtils.isEmpty(thumbFileName)) { diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java index e0b10ad..82fc1df 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java @@ -132,7 +132,7 @@ public class FriendServiceImpl extends ServiceImpl impleme friend.setUserId(userId); friend.setFriendId(friendId); User friendInfo = userMapper.selectById(friendId); - friend.setFriendHeadImage(friendInfo.getHeadImage()); + friend.setFriendHeadImage(friendInfo.getHeadImageThumb()); friend.setFriendNickName(friendInfo.getNickName()); friend.setDeleted(false); this.saveOrUpdate(friend); 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-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java index 87ce164..5e516f3 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java @@ -151,9 +151,6 @@ public class UserServiceImpl extends ServiceImpl implements Us if(!vo.getNickName().equals(sensitiveFilterUtil.filter(vo.getNickName()))){ throw new GlobalException("昵称包含敏感字符"); } - if(!vo.getSignature().equals(sensitiveFilterUtil.filter(vo.getSignature()))){ - throw new GlobalException("签名内容包含敏感字符"); - } if (!session.getUserId().equals(vo.getId())) { throw new GlobalException("不允许修改其他用户的信息"); } diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue index 1bfe6a7..a2eea21 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.handleGroupMessage(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: 60000 }) }, 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: 60000 }) }, 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); @@ -203,8 +222,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 +238,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 +336,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/components/group-item/group-item.vue b/im-uniapp/components/group-item/group-item.vue index 14b4a63..48a7481 100644 --- a/im-uniapp/components/group-item/group-item.vue +++ b/im-uniapp/components/group-item/group-item.vue @@ -1,6 +1,6 @@