diff --git a/im-commom/src/main/java/com/bx/imcommon/util/CommaTextUtils.java b/im-commom/src/main/java/com/bx/imcommon/util/CommaTextUtils.java new file mode 100644 index 0000000..f1c3354 --- /dev/null +++ b/im-commom/src/main/java/com/bx/imcommon/util/CommaTextUtils.java @@ -0,0 +1,89 @@ +package com.bx.imcommon.util; + + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 逗号分格文本处理工具类 + * + * @author: blue + * @date: 2023-11-09 09:52:49 + * @version: 1.0 + */ +public class CommaTextUtils { + + /** + * 文本转列表 + * + * @param strText 文件 + * @return 列表 + */ + public static List asList(String strText) { + if (StrUtil.isEmpty(strText)) { + return new LinkedList<>(); + } + return new LinkedList<>(Arrays.asList(strText.split(","))); + + } + + /** + * 列表转字符串,并且自动清空、去重、排序 + * + * @param texts 列表 + * @return 文本 + */ + public static String asText(Collection texts) { + if (CollUtil.isEmpty(texts)) { + return StrUtil.EMPTY; + } + return texts.stream().map(text -> StrUtil.toString(text)).filter(StrUtil::isNotEmpty).distinct().sorted().collect(Collectors.joining(",")); + } + + /** + * 追加一个单词 + * + * @param strText 文本 + * @param word 单词 + * @return 文本 + */ + public static String appendWord(String strText, T word) { + List texts = asList(strText); + texts.add(StrUtil.toString(word)); + return asText(texts); + } + + /** + * 删除一个单词 + * + * @param strText 文本 + * @param word 单词 + * @return 文本 + */ + public static String removeWord(String strText, T word) { + List texts = asList(strText); + texts.remove(StrUtil.toString(word)); + return asText(texts); + } + + /** + * 合并 + * + * @param strText1 文本1 + * @param strText2 文本2 + * @return 文本 + */ + public static String merge(String strText1, String strText2) { + List texts = asList(strText1); + texts.addAll(asList(strText2)); + return asText(texts); + } + +} 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 147e2e9..b7e7e94 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 @@ -42,6 +42,12 @@ public class GroupMessageController { return ResultUtils.success(groupMessageService.loadMessage(minId)); } + @GetMapping("/pullOfflineMessage") + @ApiOperation(value = "拉取离线消息", notes = "拉取离线消息,消息将通过webscoket异步推送") + public Result pullOfflineMessage(@RequestParam Long minId) { + groupMessageService.pullOfflineMessage(minId); + return ResultUtils.success(); + } @PutMapping("/readed") @ApiOperation(value = "消息已读", notes = "将群聊中的消息状态置为已读") 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 5f4822e..8c30239 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 @@ -43,6 +43,13 @@ public class PrivateMessageController { return ResultUtils.success(privateMessageService.loadMessage(minId)); } + @GetMapping("/pullOfflineMessage") + @ApiOperation(value = "拉取离线消息", notes = "拉取离线消息,消息将通过webscoket异步推送") + public Result pullOfflineMessage(@RequestParam Long minId) { + privateMessageService.pullOfflineMessage(minId); + return ResultUtils.success(); + } + @PutMapping("/readed") @ApiOperation(value = "消息已读", notes = "将会话中接收的消息状态置为已读") public Result readedMessage(@RequestParam Long friendId) { diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java index cf7ddaa..a7d362d 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java @@ -69,6 +69,11 @@ public class GroupMember extends Model { @TableField("quit") private Boolean quit; + /** + * 退群时间 + */ + @TableField("quit_time") + private Date quitTime; /** * 创建时间 diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java index b94b8a4..ebe02d5 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java @@ -50,6 +50,12 @@ public class GroupMessage extends Model { @TableField("send_nick_name") private String sendNickName; + /** + * 接受用户id,为空表示全体发送 + */ + @TableField("recv_ids") + private String recvIds; + /** * @用户列表 */ diff --git a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java index 69c4e06..1ca0784 100644 --- a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java +++ b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java @@ -1,6 +1,8 @@ package com.bx.implatform.enums; import lombok.AllArgsConstructor; +import lombok.Getter; + @AllArgsConstructor public enum MessageType { @@ -35,9 +37,22 @@ public enum MessageType { READED(11, "已读"), /** - * 消息已读回执(更新已读数量) + * 消息已读回执 */ RECEIPT(12, "消息已读回执"), + /** + * 时间提示 + */ + TIP_TIME(20,"时间提示"), + /** + * 文字提示 + */ + TIP_TEXT(21,"文字提示"), + + /** + * 消息加载标记 + */ + LOADDING(30,"加载中"), /** * 呼叫 */ diff --git a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java index c8cc646..7836729 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java @@ -24,6 +24,14 @@ public interface IGroupMemberService extends IService { */ List findByUserId(Long userId); + /** + * 根据用户id查询一个月内退的群 + * + * @param userId 用户id + * @return 成员列表 + */ + List findQuitInMonth(Long userId); + /** * 根据群聊id查询群聊成员(包括已退出) * @@ -32,6 +40,8 @@ public interface IGroupMemberService extends IService { */ List findByGroupId(Long groupId); + + /** * 根据群聊id查询没有退出的群聊成员id * diff --git a/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java index 39494d4..e67f809 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java @@ -32,6 +32,13 @@ public interface IGroupMessageService extends IService { */ List loadMessage(Long minId); + /** + * 拉取离线消息,只能拉取最近1个月的消息,最多拉取1000条 + * + * @param minId 消息起始id + */ + void pullOfflineMessage(Long minId); + /** * 消息已读,同步其他终端,清空未读数量 * diff --git a/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java index f57061d..4a470d1 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java @@ -44,6 +44,12 @@ public interface IPrivateMessageService extends IService { */ List loadMessage(Long minId); + /** + * 拉取离线消息,只能拉取最近1个月的消息,最多拉取1000条 + * + * @param minId 消息起始id + */ + void pullOfflineMessage(Long minId); /** * 消息已读,将整个会话的消息都置为已读状态 diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java index da0eedf..3727add 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java @@ -9,11 +9,13 @@ import com.bx.implatform.contant.RedisKey; import com.bx.implatform.entity.GroupMember; import com.bx.implatform.mapper.GroupMemberMapper; import com.bx.implatform.service.IGroupMemberService; +import com.bx.implatform.util.DateTimeUtils; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -34,7 +36,7 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = new QueryWrapper<>(); wrapper.lambda().eq(GroupMember::getGroupId, groupId) .eq(GroupMember::getUserId, userId); @@ -49,6 +51,16 @@ public class GroupMemberServiceImpl extends ServiceImpl findQuitInMonth(Long userId) { + Date monthTime = DateTimeUtils.addMonths(new Date(),-1); + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(GroupMember::getUserId, userId) + .eq(GroupMember::getQuit, true) + .ge(GroupMember::getQuitTime,monthTime); + return this.list(memberWrapper); + } + @Override public List findByGroupId(Long groupId) { LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); @@ -72,7 +84,8 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); wrapper.eq(GroupMember::getGroupId, groupId) - .set(GroupMember::getQuit, true); + .set(GroupMember::getQuit, true) + .set(GroupMember::getQuitTime,new Date()); this.update(wrapper); } @@ -82,7 +95,8 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); wrapper.eq(GroupMember::getGroupId, groupId) .eq(GroupMember::getUserId, userId) - .set(GroupMember::getQuit, true); + .set(GroupMember::getQuit, true) + .set(GroupMember::getQuitTime,new Date()); this.update(wrapper); } } 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 5b6f5f5..d64243a 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 @@ -10,8 +10,10 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bx.imclient.IMClient; import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.enums.IMTerminalType; import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.util.CommaTextUtils; import com.bx.implatform.contant.RedisKey; import com.bx.implatform.dto.GroupMessageDTO; import com.bx.implatform.entity.Group; @@ -39,6 +41,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @Slf4j @@ -63,7 +66,7 @@ public class GroupMessageServiceImpl extends ServiceImpl members = groupMemberService.findByUserId(session.getUserId()); + Map groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); + Set groupIds = groupMemberMap.keySet(); + // 只能拉取最近1个月的,最多拉取1000条 + Date minDate = DateUtils.addMonths(new Date(), -1); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId) + .gt(GroupMessage::getSendTime, minDate) + .in(GroupMessage::getGroupId, groupIds) + .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()) + .orderByDesc(GroupMessage::getId).last("limit 1000"); + List messages = this.list(wrapper); + // 通过群聊对消息进行分组 + Map> messageGroupMap = messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId)); + // 退群前的消息 + List quitMembers = groupMemberService.findQuitInMonth(session.getUserId()); + for(GroupMember quitMember: quitMembers){ + wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId) + .between(GroupMessage::getSendTime, minDate,quitMember.getQuitTime()) + .eq(GroupMessage::getGroupId, quitMember.getGroupId()) + .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()) + .orderByDesc(GroupMessage::getId) + .last("limit 100"); + List groupMessages = this.list(wrapper); + messageGroupMap.put(quitMember.getGroupId(),groupMessages); + groupMemberMap.put(quitMember.getGroupId(),quitMember); + } + // 推送消息 + AtomicInteger sendCount = new AtomicInteger(); + messageGroupMap.forEach((groupId, groupMessages) -> { + // id从小到大排序 + CollectionUtil.reverse(groupMessages); + // 填充消息状态 + 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); + // 被@用户列表 + if (StringUtils.isNotBlank(m.getAtUserIds()) && Objects.nonNull(vo)) { + List atIds = Splitter.on(",").trimResults().splitToList(m.getAtUserIds()); + vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); + } + // 填充状态 + vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.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); + } + // 推送 + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); + sendMessage.setRecvIds(Arrays.asList(session.getUserId())); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(false); + sendMessage.setData(vo); + imClient.sendGroupMessage(sendMessage); + sendCount.getAndIncrement(); + } + }); + // 关闭加载中标志 + this.sendLoadingMessage(false); + log.info("拉取离线群聊消息,用户id:{},数量:{}",session.getUserId(),sendCount.get()); + } + @Override public void readedMessage(Long groupId) { UserSession session = SessionContext.getSession(); @@ -251,7 +346,7 @@ public class GroupMessageServiceImpl extends ServiceImpl(); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setRecvIds(userIds); @@ -265,11 +360,17 @@ public class GroupMessageServiceImpl extends ServiceImpl findReadedUsers(Long groupId, Long messageId) { + UserSession session = SessionContext.getSession(); GroupMessage message = this.getById(messageId); if (Objects.isNull(message)) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息不存在"); } - // 已读位置key + // 是否在群聊里面 + GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId()); + if (Objects.isNull(member) || member.getQuit()) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面"); + } + // 已读位置key String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); // 一次获取所有用户的已读位置 Map maxIdMap = redisTemplate.opsForHash().entries(key); @@ -304,7 +405,7 @@ public class GroupMessageServiceImpl extends ServiceImpl userIds = new LinkedList<>(); maxIdMap.forEach((k, v) -> { Long userId = Long.valueOf(k.toString()); - Long maxId = Long.valueOf(v.toString()); + long maxId = Long.parseLong(v.toString()); // 发送者不计入已读人数 if (!sendId.equals(userId) && maxId >= messageId) { userIds.add(userId); @@ -313,5 +414,19 @@ public class GroupMessageServiceImpl extends ServiceImpl(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(Arrays.asList(session.getUserId())); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendGroupMessage(sendMessage); + } } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java index 58de782..f3b4b59 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java @@ -1,28 +1,30 @@ package com.bx.implatform.service.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bx.imclient.IMClient; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.util.CommaTextUtils; import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.RedisKey; -import com.bx.implatform.entity.Friend; -import com.bx.implatform.entity.Group; -import com.bx.implatform.entity.GroupMember; -import com.bx.implatform.entity.User; +import com.bx.implatform.entity.*; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; import com.bx.implatform.enums.ResultCode; import com.bx.implatform.exception.GlobalException; import com.bx.implatform.mapper.GroupMapper; -import com.bx.implatform.service.IFriendService; -import com.bx.implatform.service.IGroupMemberService; -import com.bx.implatform.service.IGroupService; -import com.bx.implatform.service.IUserService; +import com.bx.implatform.mapper.GroupMessageMapper; +import com.bx.implatform.service.*; import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; import com.bx.implatform.util.BeanUtils; import com.bx.implatform.vo.GroupInviteVO; import com.bx.implatform.vo.GroupMemberVO; +import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -44,6 +46,7 @@ import java.util.stream.Collectors; public class GroupServiceImpl extends ServiceImpl implements IGroupService { private final IUserService userService; private final IGroupMemberService groupMemberService; + private final GroupMessageMapper groupMessageMapper; private final IFriendService friendsService; private final IMClient imClient; private final RedisTemplate redisTemplate; @@ -85,7 +88,7 @@ public class GroupServiceImpl extends ServiceImpl implements } // 更新成员信息 GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(), session.getUserId()); - if (member == null) { + if (Objects.isNull(member) || member.getQuit()) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群聊的成员"); } member.setAliasName(StringUtils.isEmpty(vo.getAliasName()) ? session.getNickName() : vo.getAliasName()); @@ -104,6 +107,8 @@ public class GroupServiceImpl extends ServiceImpl implements if (!group.getOwnerId().equals(session.getUserId())) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "只有群主才有权限解除群聊"); } + // 群聊用户id + List userIds = groupMemberService.findUserIdsByGroupId(groupId); // 逻辑删除群数据 group.setDeleted(true); this.updateById(group); @@ -112,6 +117,8 @@ public class GroupServiceImpl extends ServiceImpl implements // 清理已读缓存 String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); redisTemplate.delete(key); + // 推送解散群聊提示 + this.sendTipMessage(groupId,userIds,String.format("'%s'解散了群聊",session.getNickName())); log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); } @@ -127,6 +134,8 @@ public class GroupServiceImpl extends ServiceImpl implements // 清理已读缓存 String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); redisTemplate.opsForHash().delete(key,userId.toString()); + // 推送退出群聊提示 + this.sendTipMessage(groupId,Arrays.asList(userId),"您已退出群聊"); log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); } @@ -138,27 +147,33 @@ public class GroupServiceImpl extends ServiceImpl implements throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群主,没有权限踢人"); } if (userId.equals(session.getUserId())) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "亲,不能自己踢自己哟"); + throw new GlobalException(ResultCode.PROGRAM_ERROR, "亲,不能移除自己哟"); } // 删除群聊成员 groupMemberService.removeByGroupAndUserId(groupId, userId); // 清理已读缓存 String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); redisTemplate.opsForHash().delete(key,userId.toString()); + // 推送踢出群聊提示 + this.sendTipMessage(groupId,Arrays.asList(userId),"您已被移出群聊"); log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); } @Override public GroupVO findById(Long groupId) { UserSession session = SessionContext.getSession(); - Group group = this.getById(groupId); + Group group = super.getById(groupId); + if (Objects.isNull(group)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "群组不存在"); + } GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId()); - if (member == null) { + if (Objects.isNull(member)) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "您未加入群聊"); } GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class); vo.setAliasName(member.getAliasName()); vo.setRemark(member.getRemark()); + vo.setQuit(member.getQuit()); return vo; } @@ -166,7 +181,7 @@ public class GroupServiceImpl extends ServiceImpl implements @Override public Group getById(Long groupId) { Group group = super.getById(groupId); - if (group == null) { + if (Objects.isNull(group)) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "群组不存在"); } if (group.getDeleted()) { @@ -180,6 +195,8 @@ public class GroupServiceImpl extends ServiceImpl implements UserSession session = SessionContext.getSession(); // 查询当前用户的群id列表 List groupMembers = groupMemberService.findByUserId(session.getUserId()); + // 一个月内退的群可能存在退群前的离线消息,一并返回作为前端缓存 + groupMembers.addAll(groupMemberService.findQuitInMonth(session.getUserId())); if (groupMembers.isEmpty()) { return new LinkedList<>(); } @@ -194,6 +211,7 @@ public class GroupServiceImpl extends ServiceImpl implements GroupMember member = groupMembers.stream().filter(m -> g.getId().equals(m.getGroupId())).findFirst().get(); vo.setAliasName(member.getAliasName()); vo.setRemark(member.getRemark()); + vo.setQuit(member.getQuit()); return vo; }).collect(Collectors.toList()); } @@ -202,9 +220,13 @@ public class GroupServiceImpl extends ServiceImpl implements public void invite(GroupInviteVO vo) { UserSession session = SessionContext.getSession(); Group group = this.getById(vo.getGroupId()); - if (group == null) { + if (Objects.isNull(group)) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在"); } + GroupMember member = groupMemberService.findByGroupAndUserId(vo.getGroupId(), session.getUserId()); + if (Objects.isNull(group) || member.getQuit()) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不在群聊中,邀请失败"); + } // 群聊人数校验 List members = groupMemberService.findByGroupId(vo.getGroupId()); long size = members.stream().filter(m -> !m.getQuit()).count(); @@ -234,6 +256,11 @@ public class GroupServiceImpl extends ServiceImpl implements if (!groupMembers.isEmpty()) { groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers); } + // 推送进入群聊消息 + List userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId()); + String memberNames = groupMembers.stream().map(GroupMember::getAliasName).collect(Collectors.joining(",")); + String content = String.format("'%s'邀请'%s'加入了群聊",session.getNickName(), memberNames); + this.sendTipMessage(vo.getGroupId(),userIds,content); log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), vo.getFriendIds()); } @@ -249,4 +276,33 @@ public class GroupServiceImpl extends ServiceImpl implements }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); } + private void sendTipMessage(Long groupId,List recvIds,String content){ + UserSession session = SessionContext.getSession(); + // 消息入库 + GroupMessage message = new GroupMessage(); + message.setContent(content); + message.setType(MessageType.TIP_TEXT.code()); + message.setStatus(MessageStatus.UNSEND.code()); + message.setSendTime(new Date()); + message.setSendNickName(session.getNickName()); + message.setGroupId(groupId); + message.setSendId(session.getUserId()); + message.setRecvIds(CommaTextUtils.asText(recvIds)); + groupMessageMapper.insert(message); + // 推送 + GroupMessageVO msgInfo = BeanUtils.copyProperties(message,GroupMessageVO.class); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + if(CollUtil.isEmpty(recvIds)){ + // 为空表示向全体发送 + List userIds = groupMemberService.findUserIdsByGroupId(groupId); + sendMessage.setRecvIds(userIds); + }else{ + sendMessage.setRecvIds(recvIds); + } + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(false); + imClient.sendGroupMessage(sendMessage); + } } 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 2e65510..1493c7c 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 @@ -1,5 +1,6 @@ package com.bx.implatform.service.impl; +import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; @@ -7,6 +8,8 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bx.imclient.IMClient; import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMUserInfo; import com.bx.implatform.dto.PrivateMessageDTO; @@ -23,6 +26,7 @@ import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.SensitiveFilterUtil; +import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.PrivateMessageVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -172,23 +176,81 @@ public class PrivateMessageServiceImpl extends ServiceImpl BeanUtils.copyProperties(m, PrivateMessageVO.class)).collect(Collectors.toList()); } + @Override + public void pullOfflineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + if(!imClient.isOnline(session.getUserId())){ + throw new GlobalException(ResultCode.PROGRAM_ERROR, "网络连接失败,无法拉取离线消息"); + } + // 开启加载中标志 + this.sendLoadingMessage(true); + // 查询用户好友列表 + List friends = friendService.findFriendByUserId(session.getUserId()); + if (friends.isEmpty()) { + return; + } + List friendIds = friends.stream().map(Friend::getFriendId).collect(Collectors.toList()); + // 获取当前用户的消息 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + // 只能拉取最近1个月的1000条消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + queryWrapper.gt(PrivateMessage::getId, minId) + .ge(PrivateMessage::getSendTime, minDate) + .ne(PrivateMessage::getStatus, MessageStatus.RECALL.code()) + .and(wrap -> wrap.and( + wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()) + .in(PrivateMessage::getRecvId, friendIds)) + .or(wp -> wp.eq(PrivateMessage::getRecvId, session.getUserId()) + .in(PrivateMessage::getSendId, friendIds))) + .orderByDesc(PrivateMessage::getId) + .last("limit 1000"); + List messages = this.list(queryWrapper); + // 消息顺序从小到大 + CollectionUtil.reverse(messages); + // 推送消息 + for(PrivateMessage m:messages ){ + PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); + sendMessage.setRecvId(m.getRecvId()); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setSendToSelf(false); + sendMessage.setData(vo); + sendMessage.setSendResult(true); + imClient.sendPrivateMessage(sendMessage); + } + // 关闭加载中标志 + this.sendLoadingMessage(false); + log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size()); + } @Transactional(rollbackFor = Exception.class) @Override public void readedMessage(Long friendId) { UserSession session = SessionContext.getSession(); - // 推送消息 + // 推送消息给自己,清空会话列表上的已读数量 PrivateMessageVO msgInfo = new PrivateMessageVO(); msgInfo.setType(MessageType.READED.code()); - msgInfo.setSendTime(new Date()); msgInfo.setSendId(session.getUserId()); msgInfo.setRecvId(friendId); IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setData(msgInfo); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(session.getUserId()); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + // 推送回执消息给对方,更新已读状态 + msgInfo = new PrivateMessageVO(); + msgInfo.setType(MessageType.RECEIPT.code()); + msgInfo.setSendId(session.getUserId()); + msgInfo.setRecvId(friendId); + sendMessage = new IMPrivateMessage<>(); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setRecvId(friendId); - sendMessage.setSendToSelf(true); - sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); sendMessage.setSendResult(false); + sendMessage.setData(msgInfo); imClient.sendPrivateMessage(sendMessage); // 修改消息状态为已读 LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); @@ -217,4 +279,20 @@ public class PrivateMessageServiceImpl extends ServiceImpl(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(session.getUserId()); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + } } diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java index e722c05..cbbbf14 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java @@ -1,5 +1,6 @@ package com.bx.implatform.vo; +import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -40,4 +41,11 @@ public class GroupVO { @ApiModelProperty(value = "群聊显示备注") private String remark; + @ApiModelProperty(value = "是否已删除") + private Boolean deleted; + + @ApiModelProperty(value = "是否已退出") + private Boolean quit; + + } diff --git a/im-platform/src/main/resources/db/db.sql b/im-platform/src/main/resources/db/db.sql index 776b08a..1ea4b86 100644 --- a/im-platform/src/main/resources/db/db.sql +++ b/im-platform/src/main/resources/db/db.sql @@ -55,9 +55,10 @@ create table `im_group_member`( `group_id` bigint not null comment '群id', `user_id` bigint not null comment '用户id', `alias_name` varchar(255) DEFAULT '' comment '组内显示名称', - `head_image` varchar(255) default '' comment '用户头像', + `head_image` varchar(255) DEFAULT '' comment '用户头像', `remark` varchar(255) DEFAULT '' comment '备注', `quit` tinyint(1) DEFAULT 0 comment '是否已退出', + `quit_time` datetime DEFAULT NULL comment '退出时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', key `idx_group_id`(`group_id`), key `idx_user_id`(`user_id`) @@ -68,6 +69,7 @@ create table `im_group_message`( `group_id` bigint not null comment '群id', `send_id` bigint not null comment '发送用户id', `send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称', + `recv_ids` varchar(1024) DEFAULT '' comment '接收用户id,逗号分隔,为空表示发给所有成员', `content` text comment '发送内容', `at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔', `receipt` tinyint DEFAULT 0 comment '是否回执消息', diff --git a/im-ui/src/api/enums.js b/im-ui/src/api/enums.js index 42bcbf0..3206bba 100644 --- a/im-ui/src/api/enums.js +++ b/im-ui/src/api/enums.js @@ -9,6 +9,8 @@ const MESSAGE_TYPE = { READED:11, RECEIPT:12, TIP_TIME:20, + TIP_TEXT:21, + LOADDING:30, RTC_CALL: 101, RTC_ACCEPT: 102, RTC_REJECT: 103, diff --git a/im-ui/src/components/chat/ChatGroupSide.vue b/im-ui/src/components/chat/ChatGroupSide.vue index 0904663..44e1635 100644 --- a/im-ui/src/components/chat/ChatGroupSide.vue +++ b/im-ui/src/components/chat/ChatGroupSide.vue @@ -1,12 +1,12 @@