xie.bx 3 лет назад
Родитель
Сommit
62e9c89121
39 измененных файлов с 544 добавлено и 198 удалено
  1. 1 1
      commom/src/main/java/com/lx/common/contant/RedisKey.java
  2. 5 3
      commom/src/main/java/com/lx/common/enums/WSCmdEnum.java
  3. 9 0
      commom/src/main/java/com/lx/common/model/im/LoginInfo.java
  4. 9 2
      im-platform/src/main/java/com/lx/implatform/controller/FriendController.java
  5. 10 0
      im-platform/src/main/java/com/lx/implatform/controller/GroupController.java
  6. 9 1
      im-platform/src/main/java/com/lx/implatform/controller/GroupMessageController.java
  7. 5 0
      im-platform/src/main/java/com/lx/implatform/entity/Group.java
  8. 7 0
      im-platform/src/main/java/com/lx/implatform/entity/GroupMember.java
  9. 1 1
      im-platform/src/main/java/com/lx/implatform/entity/GroupMessage.java
  10. 1 0
      im-platform/src/main/java/com/lx/implatform/service/IFriendService.java
  11. 1 1
      im-platform/src/main/java/com/lx/implatform/service/IGroupMemberService.java
  12. 3 1
      im-platform/src/main/java/com/lx/implatform/service/IGroupService.java
  13. 22 2
      im-platform/src/main/java/com/lx/implatform/service/impl/FriendServiceImpl.java
  14. 28 14
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMemberServiceImpl.java
  15. 46 4
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java
  16. 33 15
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java
  17. 3 0
      im-platform/src/main/java/com/lx/implatform/vo/GroupMemberVO.java
  18. 5 0
      im-platform/src/main/java/com/lx/implatform/vo/GroupVO.java
  19. 3 1
      im-platform/src/main/resources/db/db.sql
  20. 1 1
      im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadGroupMessageTask.java
  21. 3 5
      im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadPrivateMessageTask.java
  22. 12 11
      im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java
  23. 4 2
      im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java
  24. 6 3
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java
  25. 9 18
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java
  26. 64 0
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/LoginProcessor.java
  27. 10 2
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java
  28. 18 10
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/PrivateMessageProcessor.java
  29. 9 3
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java
  30. 21 13
      im-ui/src/api/wssocket.js
  31. 4 3
      im-ui/src/components/chat/ChatItem.vue
  32. 1 1
      im-ui/src/components/friend/FriendItem.vue
  33. 1 1
      im-ui/src/components/group/GroupItem.vue
  34. 68 35
      im-ui/src/store/chatStore.js
  35. 14 12
      im-ui/src/view/Chat.vue
  36. 13 9
      im-ui/src/view/Friend.vue
  37. 9 7
      im-ui/src/view/Group.vue
  38. 73 10
      im-ui/src/view/Home.vue
  39. 3 6
      im-ui/src/view/Login.vue

+ 1 - 1
commom/src/main/java/com/lx/common/contant/RedisKey.java

@@ -13,7 +13,7 @@ public class RedisKey {
     // 已读私聊消息id队列
     public final static String IM_READED_PRIVATE_MESSAGE_ID = "im:readed:private:id";
     // 已读群聊消息位置(已读最大id)
-    public final static String IM_GROUP_READED_POSITION = "im:readed:group:position";
+    public final static String IM_GROUP_READED_POSITION = "im:readed:group:position:";
     // 缓存前缀
     public final static String  IM_CACHE = "im:cache:";
     // 缓存是否好友:bool

+ 5 - 3
commom/src/main/java/com/lx/common/enums/WSCmdEnum.java

@@ -2,9 +2,11 @@ package com.lx.common.enums;
 
 public enum WSCmdEnum {
 
-    HEARTBEAT(0,"心跳"),
-    PRIVATE_MESSAGE(1,"私聊消息"),
-    GROUP_MESSAGE(2,"群发消息");
+    LOGIN(0,"登陆"),
+    HEART_BEAT(1,"心跳"),
+    FORCE_LOGUT(2,"强制下线"),
+    PRIVATE_MESSAGE(3,"私聊消息"),
+    GROUP_MESSAGE(4,"群发消息");
 
 
     private Integer code;

+ 9 - 0
commom/src/main/java/com/lx/common/model/im/LoginInfo.java

@@ -0,0 +1,9 @@
+package com.lx.common.model.im;
+
+import lombok.Data;
+
+@Data
+public class LoginInfo {
+
+    private long userId;
+}

+ 9 - 2
im-platform/src/main/java/com/lx/implatform/controller/FriendController.java

@@ -49,9 +49,16 @@ public class FriendController {
          return ResultUtils.success();
     }
 
-    @DeleteMapping("/delete")
+    @GetMapping("/find/{friendId}")
+    @ApiOperation(value = "查找好友信息",notes="查找好友信息")
+    public Result<FriendVO> findFriend(@NotEmpty(message = "好友id不可为空") @PathVariable("friendId") Long friendId){
+        return ResultUtils.success(friendService.findFriend(friendId));
+    }
+
+
+    @DeleteMapping("/delete/{friendId}")
     @ApiOperation(value = "删除好友",notes="解除好友关系")
-    public Result delFriend(@NotEmpty(message = "好友id不可为空") @RequestParam("friendId") Long friendId){
+    public Result delFriend(@NotEmpty(message = "好友id不可为空") @PathVariable("friendId") Long friendId){
         friendService.delFriend(friendId);
         return ResultUtils.success();
     }

+ 10 - 0
im-platform/src/main/java/com/lx/implatform/controller/GroupController.java

@@ -3,10 +3,13 @@ package com.lx.implatform.controller;
 
 import com.lx.common.result.Result;
 import com.lx.common.result.ResultUtils;
+import com.lx.common.util.BeanUtils;
+import com.lx.implatform.entity.Group;
 import com.lx.implatform.service.IGroupService;
 import com.lx.implatform.vo.GroupInviteVO;
 import com.lx.implatform.vo.GroupMemberVO;
 import com.lx.implatform.vo.GroupVO;
+import com.lx.implatform.vo.UserVO;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -15,6 +18,7 @@ import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
 
 
 @RestController
@@ -43,6 +47,12 @@ public class GroupController {
         return ResultUtils.success();
     }
 
+    @ApiOperation(value = "查询群聊",notes="查询单个群聊信息")
+    @GetMapping("/find/{groupId}")
+    public Result<GroupVO> findGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){
+        return ResultUtils.success(groupService.findById(groupId));
+    }
+
     @ApiOperation(value = "查询群聊列表",notes="查询群聊列表")
     @GetMapping("/list")
     public Result<List<GroupVO>> findGroups(){

+ 9 - 1
im-platform/src/main/java/com/lx/implatform/controller/GroupMessageController.java

@@ -19,7 +19,7 @@ import javax.validation.Valid;
 
 
 @RestController
-@RequestMapping("/group/message")
+@RequestMapping("/message/group")
 public class GroupMessageController {
 
     @Autowired
@@ -33,5 +33,13 @@ public class GroupMessageController {
         return ResultUtils.success();
     }
 
+
+    @PostMapping("/pullUnreadMessage")
+    @ApiOperation(value = "拉取未读消息",notes="拉取未读消息")
+    public Result pullUnreadMessage(){
+        groupMessageService.pullUnreadMessage();
+        return ResultUtils.success();
+    }
+
 }
 

+ 5 - 0
im-platform/src/main/java/com/lx/implatform/entity/Group.java

@@ -63,6 +63,11 @@ public class Group extends Model<Group> {
     @TableField("notice")
     private String notice;
 
+    /**
+     * 是否已删除
+     */
+    @TableField("deleted")
+    private Boolean deleted;
 
     /**
      * 创建时间

+ 7 - 0
im-platform/src/main/java/com/lx/implatform/entity/GroupMember.java

@@ -68,6 +68,13 @@ public class GroupMember extends Model<GroupMember> {
     @TableField("remark")
     private String remark;
 
+    /**
+     * 是否已离开群聊
+     */
+    @TableField("quit")
+    private Boolean quit;
+
+
     /**
      * 创建时间
      */

+ 1 - 1
im-platform/src/main/java/com/lx/implatform/entity/GroupMessage.java

@@ -58,7 +58,7 @@ public class GroupMessage extends Model<GroupMessage> {
      * 消息类型 0:文字 1:图片 2:文件
      */
     @TableField("type")
-    private Boolean type;
+    private Integer type;
 
     /**
      * 发送时间

+ 1 - 0
im-platform/src/main/java/com/lx/implatform/service/IFriendService.java

@@ -26,4 +26,5 @@ public interface IFriendService extends IService<Friend> {
 
     void update(FriendVO vo);
 
+    FriendVO findFriend(Long friendId);
 }

+ 1 - 1
im-platform/src/main/java/com/lx/implatform/service/IGroupMemberService.java

@@ -28,7 +28,7 @@ public interface IGroupMemberService extends IService<GroupMember> {
 
     boolean save(GroupMember member);
 
-    boolean saveBatch(Long groupId,List<GroupMember> members);
+    boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members);
 
     void removeByGroupId(Long groupId);
 

+ 3 - 1
im-platform/src/main/java/com/lx/implatform/service/IGroupService.java

@@ -31,7 +31,9 @@ public interface IGroupService extends IService<Group> {
 
     void invite(GroupInviteVO vo);
 
-    Group findById(Long id);
+    Group GetById(Long groupId);
+
+    GroupVO findById(Long groupId);
 
     List<GroupMemberVO> findGroupMembers(Long groupId);
 }

+ 22 - 2
im-platform/src/main/java/com/lx/implatform/service/impl/FriendServiceImpl.java

@@ -11,6 +11,7 @@ import com.lx.implatform.mapper.FriendMapper;
 import com.lx.implatform.service.IFriendService;
 import com.lx.implatform.service.IUserService;
 import com.lx.implatform.session.SessionContext;
+import com.lx.implatform.session.UserSession;
 import com.lx.implatform.vo.FriendVO;
 import org.springframework.aop.framework.AopContext;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -54,8 +55,9 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
             throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许添加自己为好友");
         }
         // 互相绑定好友关系
-        bindFriend(userId,friendId);
-        bindFriend(friendId,userId);
+        FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
+        proxy.bindFriend(userId,friendId);
+        proxy.bindFriend(friendId,userId);
     }
 
 
@@ -99,6 +101,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
         this.updateById(f);
     }
 
+    @CacheEvict(key="#userId+':'+#friendId")
     public void bindFriend(Long userId, Long friendId) {
         QueryWrapper<Friend> queryWrapper = new QueryWrapper<>();
         queryWrapper.lambda()
@@ -128,4 +131,21 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
     }
 
 
+    @Override
+    public FriendVO findFriend(Long friendId) {
+        UserSession session = SessionContext.getSession();
+        QueryWrapper<Friend> wrapper = new QueryWrapper<>();
+        wrapper.lambda()
+                .eq(Friend::getUserId,session.getId())
+                .eq(Friend::getFriendId,friendId);
+        Friend friend = this.getOne(wrapper);
+        if(friend == null){
+            throw new GlobalException(ResultCode.PROGRAM_ERROR,"对方不是您的好友");
+        }
+        FriendVO vo = new FriendVO();
+        vo.setId(friend.getFriendId());
+        vo.setHeadImage(friend.getFriendHeadImage());
+        vo.setNickName(friend.getFriendNickName());
+        return  vo;
+    }
 }

+ 28 - 14
im-platform/src/main/java/com/lx/implatform/service/impl/GroupMemberServiceImpl.java

@@ -1,6 +1,7 @@
 package com.lx.implatform.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.lx.common.contant.RedisKey;
 import com.lx.implatform.entity.GroupMember;
 import com.lx.implatform.mapper.GroupMemberMapper;
@@ -42,8 +43,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
      */
     @CacheEvict(key="#groupId")
     @Override
-    public boolean saveBatch(Long groupId,List<GroupMember> members) {
-        return super.saveBatch(members);
+    public boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members) {
+        return super.saveOrUpdateBatch(members);
     }
 
     /**
@@ -70,12 +71,13 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
     @Override
     public List<GroupMember> findByUserId(Long userId) {
         QueryWrapper<GroupMember> memberWrapper = new QueryWrapper();
-        memberWrapper.lambda().eq(GroupMember::getUserId, userId);
+        memberWrapper.lambda().eq(GroupMember::getUserId, userId)
+                .eq(GroupMember::getQuit,false);
         return this.list(memberWrapper);
     }
 
     /**
-     * 根据群聊id查询群聊成员
+     * 根据群聊id查询群聊成员(包括已退出)
      *
      * @param groupId 群聊id
      * @return
@@ -87,16 +89,26 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
         return this.list(memberWrapper);
     }
 
-
+    /**
+     * 根据群聊id查询没有退出的群聊成员id
+     *
+     * @param groupId 群聊id
+     * @return
+     */
     @Cacheable(key="#groupId")
     @Override
     public List<Long> findUserIdsByGroupId(Long groupId) {
-        List<GroupMember> members = this.findByGroupId(groupId);
+        QueryWrapper<GroupMember> memberWrapper = new QueryWrapper();
+        memberWrapper.lambda().eq(GroupMember::getGroupId, groupId)
+                .eq(GroupMember::getQuit,false);
+        List<GroupMember> members = this.list(memberWrapper);
         return members.stream().map(m->m.getUserId()).collect(Collectors.toList());
     }
 
+
+
     /**
-     *根据群聊id删除成员信息
+     *根据群聊id删除移除成员
      *
      * @param groupId  群聊id
      * @return
@@ -104,13 +116,14 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
     @CacheEvict(key = "#groupId")
     @Override
     public void removeByGroupId(Long groupId) {
-        QueryWrapper<GroupMember> wrapper = new QueryWrapper();
-        wrapper.lambda().eq(GroupMember::getGroupId,groupId);
-        this.remove(wrapper);
+        UpdateWrapper<GroupMember> wrapper = new UpdateWrapper();
+        wrapper.lambda().eq(GroupMember::getGroupId,groupId)
+                .set(GroupMember::getQuit,true);
+        this.update(wrapper);
     }
 
     /**
-     *根据群聊id和用户id删除成员信息
+     *根据群聊id和用户id移除成员
      *
      * @param groupId  群聊id
      * @param userId  用户id
@@ -119,9 +132,10 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
     @CacheEvict(key = "#groupId")
     @Override
     public void removeByGroupAndUserId(Long groupId, Long userId) {
-        QueryWrapper<GroupMember> wrapper = new QueryWrapper<>();
+        UpdateWrapper<GroupMember> wrapper = new UpdateWrapper<>();
         wrapper.lambda().eq(GroupMember::getGroupId,groupId)
-                .eq(GroupMember::getUserId,userId);
-        this.remove(wrapper);
+                .eq(GroupMember::getUserId,userId)
+                .set(GroupMember::getQuit,true);
+        this.update(wrapper);
     }
 }

+ 46 - 4
im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java

@@ -1,10 +1,12 @@
 package com.lx.implatform.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.lx.common.contant.RedisKey;
 import com.lx.common.enums.ResultCode;
 import com.lx.common.model.im.GroupMessageInfo;
 import com.lx.common.util.BeanUtils;
 import com.lx.implatform.entity.Group;
+import com.lx.implatform.entity.GroupMember;
 import com.lx.implatform.entity.GroupMessage;
 import com.lx.implatform.exception.GlobalException;
 import com.lx.implatform.mapper.GroupMessageMapper;
@@ -21,6 +23,7 @@ import org.springframework.stereotype.Service;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
 
 
 @Service
@@ -45,7 +48,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
     @Override
     public void sendMessage(GroupMessageVO vo) {
         Long userId = SessionContext.getSession().getId();
-        Group group = groupService.findById(vo.getGroupId());
+        Group group = groupService.getById(vo.getGroupId());
         if(group == null){
             throw  new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在或已解散");
         }
@@ -57,7 +60,15 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         // 根据群聊每个成员所连的IM-server,进行分组
         Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>();
         List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
+        if(!userIds.contains(userId)){
+            throw  new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法发送消息");
+        }
+
         userIds.parallelStream().forEach(id->{
+            if(id == userId){
+                // 自己不需要推送给自己
+                return;
+            }
             String key = RedisKey.IM_USER_SERVER_ID + id;
             Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
             if(serverId != null){
@@ -73,8 +84,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         // 逐个server发送
         for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) {
             GroupMessageInfo  msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
-            msgInfo.setRecvIds(entry.getValue());
-            String key = RedisKey.IM_UNREAD_PRIVATE_MESSAGE +entry.getKey();
+            msgInfo.setRecvIds(new LinkedList<>(entry.getValue()));
+            String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey();
             redisTemplate.opsForList().rightPush(key,msgInfo);
         }
     }
@@ -86,6 +97,37 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
      */
     @Override
     public void pullUnreadMessage() {
-
+        Long userId = SessionContext.getSession().getId();
+        String key = RedisKey.IM_USER_SERVER_ID+userId;
+        Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
+        if(serverId == null){
+            throw new GlobalException(ResultCode.PROGRAM_ERROR,"用户未建立连接");
+        }
+        List<Long> recvIds = new LinkedList();
+        recvIds.add(userId);
+        List<GroupMember> members = groupMemberService.findByUserId(userId);
+        for(GroupMember member:members){
+            // 获取群聊已读的最大消息id,只推送未读消息
+            key = RedisKey.IM_GROUP_READED_POSITION + member.getGroupId()+":"+userId;
+            Integer maxReadedId = (Integer)redisTemplate.opsForValue().get(key);
+            QueryWrapper<GroupMessage> wrapper = new QueryWrapper();
+            wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId());
+            if(maxReadedId!=null){
+                wrapper.lambda().gt(GroupMessage::getId,maxReadedId);
+            }
+            wrapper.last("limit 100");
+            List<GroupMessage> messages = this.list(wrapper);
+            if(messages.isEmpty()){
+                continue;
+            }
+            // 组装消息,准备推送
+            List<GroupMessageInfo> messageInfos = messages.stream().map(m->{
+                GroupMessageInfo  msgInfo = BeanUtils.copyProperties(m, GroupMessageInfo.class);
+                msgInfo.setRecvIds(recvIds);
+                return  msgInfo;
+            }).collect(Collectors.toList());
+            key = RedisKey.IM_UNREAD_GROUP_MESSAGE + serverId;
+            redisTemplate.opsForList().rightPushAll(key,messageInfos.toArray());
+        }
     }
 }

+ 33 - 15
im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java

@@ -33,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
 import java.lang.reflect.Member;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 
@@ -126,12 +127,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
         if(group.getOwnerId() != session.getId()){
             throw  new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊");
         }
-        // 删除群数据
-        this.removeById(groupId);
-        // 删除成员数据
-        groupMemberService.removeByGroupId(groupId);
+        // 逻辑删除群数据
+        group.setDeleted(true);
+        this.updateById(group);
     }
 
+
     /**
      *退出群聊
      *
@@ -152,6 +153,24 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
         groupMemberService.removeByGroupAndUserId(groupId,session.getId());
     }
 
+
+    @Override
+    public GroupVO findById(Long groupId) {
+        UserSession session = SessionContext.getSession();
+        Group group = super.getById(groupId);
+        if(group == null){
+            throw  new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在");
+        }
+        GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getId());
+        if(member == null){
+            throw  new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊");
+        }
+        GroupVO vo = BeanUtils.copyProperties(group,GroupVO.class);
+        vo.setAliasName(member.getAliasName());
+        vo.setRemark(member.getRemark());
+        return  vo;
+    }
+
     /**
      *根据id查找群聊,并进行缓存
      *
@@ -160,10 +179,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
      */
     @Cacheable(value = "#groupId")
     @Override
-    public  Group findById(Long groupId){
+    public  Group GetById(Long groupId){
         return super.getById(groupId);
     }
 
+
+
     /**
      * 查询当前用户的所有群聊
      *
@@ -208,16 +229,11 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
         }
         // 群聊人数校验
         List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId());
-        if(vo.getFriendIds().size() + members.size() > Constant.MAX_GROUP_MEMBER){
+        long size = members.stream().filter(m->!m.getQuit()).count();
+        if(vo.getFriendIds().size() + size > Constant.MAX_GROUP_MEMBER){
             throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊人数不能大于"+Constant.MAX_GROUP_MEMBER+"人");
         }
-        // 已经在群里面用户,不可重复加入
-        Boolean flag = vo.getFriendIds().stream().anyMatch(id->{
-           return  members.stream().anyMatch(m->m.getUserId()==id);
-        });
-        if(flag){
-            throw new GlobalException(ResultCode.PROGRAM_ERROR, "部分用户已经在群中,邀请失败");
-        }
+
         // 找出好友信息
         List<Friend> friends = friendsService.findFriendByUserId(session.getId());
         List<Friend> friendsList = vo.getFriendIds().stream().map(id ->
@@ -228,16 +244,18 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
         // 批量保存成员数据
         List<GroupMember> groupMembers = friendsList.stream()
                 .map(f -> {
-                    GroupMember groupMember = new GroupMember();
+                    Optional<GroupMember> optional =  members.stream().filter(m->m.getUserId()==f.getFriendId()).findFirst();
+                    GroupMember groupMember = optional.isPresent()? optional.get():new GroupMember();
                     groupMember.setGroupId(vo.getGroupId());
                     groupMember.setUserId(f.getFriendId());
                     groupMember.setAliasName(f.getFriendNickName());
                     groupMember.setRemark(group.getName());
                     groupMember.setHeadImage(f.getFriendHeadImage());
+                    groupMember.setQuit(false);
                     return groupMember;
                 }).collect(Collectors.toList());
         if(!groupMembers.isEmpty()) {
-            groupMemberService.saveBatch(group.getId(),groupMembers);
+            groupMemberService.saveOrUpdateBatch(group.getId(),groupMembers);
         }
     }
 

+ 3 - 0
im-platform/src/main/java/com/lx/implatform/vo/GroupMemberVO.java

@@ -18,6 +18,9 @@ public class GroupMemberVO {
     @ApiModelProperty("头像")
     private String headImage;
 
+    @ApiModelProperty("是否已退出")
+    private Boolean quit;
+
     @ApiModelProperty("备注")
     private String remark;
 

+ 5 - 0
im-platform/src/main/java/com/lx/implatform/vo/GroupVO.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
+import org.hibernate.validator.constraints.Length;
 
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
@@ -19,6 +20,7 @@ public class GroupVO {
     @ApiModelProperty(value = "群id")
     private Long id;
 
+    @Length(max=20,message = "群名称长度不能大于20")
     @NotEmpty(message = "群名称不可为空")
     @ApiModelProperty(value = "群名称")
     private String name;
@@ -33,12 +35,15 @@ public class GroupVO {
     @ApiModelProperty(value = "头像缩略图")
     private String headImageThumb;
 
+    @Length(max=1024,message = "群聊显示长度不能大于1024")
     @ApiModelProperty(value = "群公告")
     private String notice;
 
+    @Length(max=20,message = "群聊显示长度不能大于20")
     @ApiModelProperty(value = "用户在群显示昵称")
     private String aliasName;
 
+    @Length(max=20,message = "群聊显示长度不能大于20")
     @ApiModelProperty(value = "群聊显示备注")
     private String remark;
 

+ 3 - 1
im-platform/src/main/resources/db/db.sql

@@ -45,6 +45,7 @@ create table `im_group`(
     `head_image_thumb` varchar(255) default '' comment '群头像缩略图',
     `notice` varchar(1024)  default '' comment '群公告',
     `remark` varchar(255) default '' comment '群备注',
+    `deleted` tinyint(1) DEFAULT 0   comment '是否已删除',
     `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间'
 )ENGINE=InnoDB CHARSET=utf8mb3 comment '群';
 
@@ -55,6 +56,7 @@ create table `im_group_member`(
     `alias_name` varchar(255) DEFAULT '' comment '组内显示名称',
     `head_image` varchar(255) default '' comment '用户头像',
     `remark` varchar(255) DEFAULT '' comment '备注',
+    `quit` tinyint(1) DEFAULT 0  comment '是否已退出',
     `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',
     key `idx_group_id`(`group_id`),
     key `idx_user_id`(`user_id`)
@@ -63,7 +65,7 @@ create table `im_group_member`(
 create table `im_group_message`(
     `id` bigint not null auto_increment primary key comment 'id',
     `group_id` bigint not null  comment '群id',
-    `send_user_id` bigint not null  comment '发送用户id',
+    `send_id` bigint not null  comment '发送用户id',
     `content` text   comment '发送内容',
     `type`  tinyint(1) NOT NULL  comment '消息类型 0:文字 1:图片 2:文件',
     `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间',

+ 1 - 1
im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadGroupMessageTask.java

@@ -36,7 +36,7 @@ public class PullUnreadGroupMessageTask extends  AbstractPullMessageTask {
         for(Object o: messageInfos){
             redisTemplate.opsForList().leftPop(key);
             GroupMessageInfo messageInfo = (GroupMessageInfo)o;
-            MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.PRIVATE_MESSAGE);
+            MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.GROUP_MESSAGE);
             processor.process(null,messageInfo);
         }
     }

+ 3 - 5
im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadPrivateMessageTask.java

@@ -30,18 +30,16 @@ public class PullUnreadPrivateMessageTask extends  AbstractPullMessageTask {
 
     @Override
     public void pullMessage() {
-        log.info(Thread.currentThread().getName());
         // 从redis拉取未读消息
         String key = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + WSServer.getServerId();
         List messageInfos = redisTemplate.opsForList().range(key,0,-1);
         for(Object o: messageInfos){
             redisTemplate.opsForList().leftPop(key);
             PrivateMessageInfo messageInfo = (PrivateMessageInfo)o;
-            ChannelHandlerContext ctx = WebsocketChannelCtxHloder.getChannelCtx(messageInfo.getRecvId());
-            if(ctx != null){
+
                 MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.PRIVATE_MESSAGE);
-                processor.process(ctx,messageInfo);
-            }
+                processor.process(null,messageInfo);
+
         }
     }
 

+ 12 - 11
im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java

@@ -31,10 +31,8 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<SendInfo> {
     @Override
     protected void channelRead0(ChannelHandlerContext ctx, SendInfo sendInfo) throws Exception {
         // 创建处理器进行处理
-        HashMap map = (HashMap)sendInfo.getData();
-        HeartbeatInfo beatInfo = BeanUtil.fillBeanWithMap(map, new HeartbeatInfo(), false);
         MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.fromCode(sendInfo.getCmd()));
-        processor.process(ctx,beatInfo);
+        processor.process(ctx,processor.transForm(sendInfo.getData()));
     }
 
     /**
@@ -64,16 +62,19 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<SendInfo> {
 
     @Override
     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-
         AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID");
         Long userId = ctx.channel().attr(attr).get();
-        // 移除channel
-        WebsocketChannelCtxHloder.removeChannelCtx(userId);
-        // 用户下线
-        RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
-        String key = RedisKey.IM_USER_SERVER_ID + userId;
-        redisTemplate.delete(key);
-        log.info(ctx.channel().id().asLongText() + "断开连接");
+        ChannelHandlerContext context = WebsocketChannelCtxHloder.getChannelCtx(userId);
+        // 判断一下,避免异地登录导致的误删
+        if(context != null && ctx.channel().id().equals(context.channel().id())){
+            // 移除channel
+            WebsocketChannelCtxHloder.removeChannelCtx(userId);
+            // 用户下线
+            RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
+            String key = RedisKey.IM_USER_SERVER_ID + userId;
+            redisTemplate.delete(key);
+            log.info("断开连接,userId:{}",userId);
+        }
     }
 
     @Override

+ 4 - 2
im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java

@@ -2,8 +2,7 @@ package com.lx.implatform.imserver.websocket;
 
 import io.netty.channel.ChannelHandlerContext;
 
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class WebsocketChannelCtxHloder {
@@ -19,6 +18,9 @@ public class WebsocketChannelCtxHloder {
         channelMap.remove(userId);
     }
 
+
+
+
     public static ChannelHandlerContext  getChannelCtx(Long userId){
         return channelMap.get(userId);
     }

+ 6 - 3
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java

@@ -16,7 +16,7 @@ import java.util.List;
 
 @Slf4j
 @Component
-public class GroupMessageProcessor implements  MessageProcessor<GroupMessageInfo> {
+public class GroupMessageProcessor extends  MessageProcessor<GroupMessageInfo> {
 
     @Autowired
     private RedisTemplate<String,Object> redisTemplate;
@@ -24,7 +24,7 @@ public class GroupMessageProcessor implements  MessageProcessor<GroupMessageInfo
     @Async
     @Override
     public void process(ChannelHandlerContext ctx, GroupMessageInfo data) {
-        log.info("接收到群消息,发送者:{},群id:{},内容:{}",data.getSendId(),data.getGroupId(),data.getContent());
+        log.info("接收到群消息,发送者:{},群id:{},接收id:{},内容:{}",data.getSendId(),data.getGroupId(),data.getRecvIds(),data.getContent());
         List<Long> recvIds = data.getRecvIds();
         // 接收者id列表不需要传输,节省带宽
         data.setRecvIds(null);
@@ -37,9 +37,12 @@ public class GroupMessageProcessor implements  MessageProcessor<GroupMessageInfo
                 sendInfo.setData(data);
                 channelCtx.channel().writeAndFlush(sendInfo);
                 // 设置已读最大id
-                String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId();
+                String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId;
                 redisTemplate.opsForValue().set(key,data.getId());
+            }else {
+                log.error("未找到WS连接,发送者:{},群id:{},接收id:{},内容:{}",data.getSendId(),data.getGroupId(),data.getRecvIds());
             }
+
         }
     }
 

+ 9 - 18
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java

@@ -1,25 +1,21 @@
 package com.lx.implatform.imserver.websocket.processor;
 
 import cn.hutool.core.bean.BeanUtil;
-import com.lx.common.contant.RedisKey;
 import com.lx.common.enums.WSCmdEnum;
 import com.lx.common.model.im.HeartbeatInfo;
 import com.lx.common.model.im.SendInfo;
-import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder;
 import com.lx.implatform.imserver.websocket.WebsocketServer;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.util.AttributeKey;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
 import java.util.HashMap;
-import java.util.concurrent.TimeUnit;
 
 @Slf4j
 @Component
-public class HeartbeatProcessor implements  MessageProcessor<HeartbeatInfo> {
+public class HeartbeatProcessor extends   MessageProcessor<HeartbeatInfo> {
 
 
     @Autowired
@@ -30,22 +26,17 @@ public class HeartbeatProcessor implements  MessageProcessor<HeartbeatInfo> {
 
     @Override
     public void process(ChannelHandlerContext ctx, HeartbeatInfo beatInfo) {
-        log.info("接收到心跳,userId:{}",beatInfo.getUserId());
-
-        // 绑定用户和channel
-        WebsocketChannelCtxHloder.addChannelCtx(beatInfo.getUserId(),ctx);
-        // 设置属性
-        AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID");
-        ctx.channel().attr(attr).set(beatInfo.getUserId());
-
-        // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期
-        String key = RedisKey.IM_USER_SERVER_ID+beatInfo.getUserId();
-        redisTemplate.opsForValue().set(key, WSServer.getServerId(),15, TimeUnit.SECONDS);
-
         // 响应ws
         SendInfo sendInfo = new SendInfo();
-        sendInfo.setCmd(WSCmdEnum.HEARTBEAT.getCode());
+        sendInfo.setCmd(WSCmdEnum.HEART_BEAT.getCode());
         ctx.channel().writeAndFlush(sendInfo);
     }
 
+
+    @Override
+    public HeartbeatInfo transForm(Object o) {
+        HashMap map = (HashMap)o;
+        HeartbeatInfo heartbeatInfo = BeanUtil.fillBeanWithMap(map, new HeartbeatInfo(), false);
+        return  heartbeatInfo;
+    }
 }

+ 64 - 0
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/LoginProcessor.java

@@ -0,0 +1,64 @@
+package com.lx.implatform.imserver.websocket.processor;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.lx.common.contant.RedisKey;
+import com.lx.common.enums.WSCmdEnum;
+import com.lx.common.model.im.HeartbeatInfo;
+import com.lx.common.model.im.LoginInfo;
+import com.lx.common.model.im.SendInfo;
+import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder;
+import com.lx.implatform.imserver.websocket.WebsocketServer;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.AttributeKey;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+public class LoginProcessor extends   MessageProcessor<LoginInfo> {
+
+
+    @Autowired
+    private WebsocketServer WSServer;
+
+    @Autowired
+    RedisTemplate<String,Object> redisTemplate;
+
+    @Override
+    synchronized public void process(ChannelHandlerContext ctx, LoginInfo loginInfo) {
+        log.info("用户登录,userId:{}",loginInfo.getUserId());
+        ChannelHandlerContext context = WebsocketChannelCtxHloder.getChannelCtx(loginInfo.getUserId());
+        if(context != null){
+            // 不允许多地登录,强制下线
+            SendInfo sendInfo = new SendInfo();
+            sendInfo.setCmd(WSCmdEnum.FORCE_LOGUT.getCode());
+            context.channel().writeAndFlush(sendInfo);
+        }
+
+        // 绑定用户和channel
+        WebsocketChannelCtxHloder.addChannelCtx(loginInfo.getUserId(),ctx);
+        // 设置属性
+        AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID");
+        ctx.channel().attr(attr).set(loginInfo.getUserId());
+        // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期
+        String key = RedisKey.IM_USER_SERVER_ID+loginInfo.getUserId();
+        redisTemplate.opsForValue().set(key, WSServer.getServerId());
+        // 响应ws
+        SendInfo sendInfo = new SendInfo();
+        sendInfo.setCmd(WSCmdEnum.LOGIN.getCode());
+        ctx.channel().writeAndFlush(sendInfo);
+    }
+
+
+    @Override
+    public LoginInfo transForm(Object o) {
+        HashMap map = (HashMap)o;
+        LoginInfo loginInfo = BeanUtil.fillBeanWithMap(map, new LoginInfo(), false);
+        return  loginInfo;
+    }
+}

+ 10 - 2
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java

@@ -1,9 +1,17 @@
 package com.lx.implatform.imserver.websocket.processor;
 
+
 import io.netty.channel.ChannelHandlerContext;
 
-public interface MessageProcessor<T> {
+public abstract class MessageProcessor<T> {
+
+    public void process(ChannelHandlerContext ctx,T data){}
+
+    public void process(T data){}
+
+     public T transForm(Object o){
+         return (T)o;
+     }
 
-    void process(ChannelHandlerContext ctx,T data);
 
 }

+ 18 - 10
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/PrivateMessageProcessor.java

@@ -4,6 +4,7 @@ import com.lx.common.contant.RedisKey;
 import com.lx.common.enums.WSCmdEnum;
 import com.lx.common.model.im.SendInfo;
 import com.lx.common.model.im.PrivateMessageInfo;
+import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder;
 import io.netty.channel.ChannelHandlerContext;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -11,26 +12,33 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
 
+import java.util.List;
+
 @Slf4j
 @Component
-public class PrivateMessageProcessor implements  MessageProcessor<PrivateMessageInfo> {
+public class PrivateMessageProcessor extends  MessageProcessor<PrivateMessageInfo> {
 
     @Autowired
     private RedisTemplate<String,Object> redisTemplate;
 
-    @Async
     @Override
     public void process(ChannelHandlerContext ctx, PrivateMessageInfo data) {
         log.info("接收到消息,发送者:{},接收者:{},内容:{}",data.getSendId(),data.getRecvId(),data.getContent());
-        // 推送消息到用户
-        SendInfo sendInfo = new SendInfo();
-        sendInfo.setCmd(WSCmdEnum.PRIVATE_MESSAGE.getCode());
-        sendInfo.setData(data);
-        ctx.channel().writeAndFlush(sendInfo);
+        // 一个用户可以同时登陆,所以有多个channel
+        ChannelHandlerContext channelCtx = WebsocketChannelCtxHloder.getChannelCtx(data.getRecvId());
+        if(channelCtx != null ){
+            // 推送消息到用户
+            SendInfo sendInfo = new SendInfo();
+            sendInfo.setCmd(WSCmdEnum.PRIVATE_MESSAGE.getCode());
+            sendInfo.setData(data);
+            channelCtx.channel().writeAndFlush(sendInfo);
+            // 已读消息推送至redis,等待更新数据库
+            String key = RedisKey.IM_READED_PRIVATE_MESSAGE_ID;
+            redisTemplate.opsForList().rightPush(key,data.getId());
+        }else{
+            log.error("未找到WS连接,发送者:{},接收者:{},内容:{}",data.getSendId(),data.getRecvId(),data.getContent());
+        }
 
-        // 已读消息推送至redis,等待更新数据库
-        String key = RedisKey.IM_READED_PRIVATE_MESSAGE_ID;
-        redisTemplate.opsForList().rightPush(key,data.getId());
     }
 
 }

+ 9 - 3
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java

@@ -8,11 +8,17 @@ public class ProcessorFactory {
     public static MessageProcessor createProcessor(WSCmdEnum cmd){
         MessageProcessor  processor = null;
         switch (cmd){
-            case HEARTBEAT:
-                processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean("heartbeatProcessor");
+            case LOGIN:
+                processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class);
+                break;
+            case HEART_BEAT:
+                processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class);
                 break;
             case PRIVATE_MESSAGE:
-                processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean("privateMessageProcessor");
+                processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(PrivateMessageProcessor.class);
+                break;
+            case GROUP_MESSAGE:
+                processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(GroupMessageProcessor.class);
                 break;
             default:
                 break;

+ 21 - 13
im-ui/src/api/wssocket.js

@@ -1,12 +1,11 @@
 var websock = null;
 let rec; //断线重连后,延迟5秒重新创建WebSocket连接  rec用来存储延迟请求的代码
 let isConnect = false; //连接标识 避免重复连接
-let isCompleteConnect = false; //完全连接标识(接收到心跳)
 let wsurl = "";
 let $store = null;
 let messageCallBack = null;
 let openCallBack = null;
-
+let hasLogin = false;
 
 let createWebSocket = (url, store) => {
 	$store = store;
@@ -17,16 +16,19 @@ let createWebSocket = (url, store) => {
 let initWebSocket = () => {
 	try {
 		console.log("初始化WebSocket");
-		isCompleteConnect = false;
+		hasLogin = false;
 		websock = new WebSocket(wsurl);
 		websock.onmessage = function(e) {
 			let msg = JSON.parse(e.data)
 			if (msg.cmd == 0) {
-				if(!isCompleteConnect){
-					// 第一次上传心跳成功才算连接完成
-					isCompleteConnect = true;
-					openCallBack && openCallBack();
-				}
+				hasLogin = true;
+				heartCheck.start()
+				console.log('WebSocket登录成功')
+				// 登录成功才算连接完成
+				openCallBack && openCallBack();
+			}
+			else if(msg.cmd==1){
+				// 重新开启心跳定时
 				heartCheck.reset();
 			} else {
 				// 其他消息转发出去
@@ -36,12 +38,17 @@ let initWebSocket = () => {
 		websock.onclose = function(e) {
 			console.log('WebSocket连接关闭')
 			isConnect = false; //断开后修改标识
-			reConnect();
 		}
 		websock.onopen = function() {
 			console.log("WebSocket连接成功");
 			isConnect = true;
-			heartCheck.start()
+			// 发送登录命令
+			let loginInfo = {
+				cmd: 0,
+				data: {userId: $store.state.userStore.userInfo.id}
+			};
+			websock.send(JSON.stringify(loginInfo));
+			
 		}
 
 		// 连接发生错误的回调方法
@@ -69,6 +76,8 @@ let reConnect = () => {
 let closeWebSocket = () => {
 	websock.close();
 };
+
+
 //心跳设置
 var heartCheck = {
 	timeout: 5000, //每段时间发送一次心跳包 这里设置为20s
@@ -77,14 +86,13 @@ var heartCheck = {
 		if(isConnect){
 			console.log('发送WebSocket心跳')
 			let heartBeat = {
-				cmd: 0,
+				cmd: 1,
 				data: {
 					userId: $store.state.userStore.userInfo.id
 				}
 			};
 			websock.send(JSON.stringify(heartBeat))
 		}
-		
 	},
 
 	reset: function(){
@@ -125,7 +133,7 @@ function onmessage(callback) {
 
 function onopen(callback) {
 	openCallBack = callback;
-	if (isCompleteConnect) {
+	if (hasLogin) {
 		openCallBack();
 	}
 }

+ 4 - 3
im-ui/src/components/chat/ChatItem.vue

@@ -1,6 +1,6 @@
 <template>
 
-	<div class="item" :class="active ? 'active' : ''">
+	<div class="chat-item" :class="active ? 'active' : ''">
 		<div class="left">
 			<head-image :url="chat.headImage" :size="40">
 				
@@ -55,7 +55,7 @@
 </script>
 
 <style scode lang="scss">
-	.item {
+	.chat-item {
 		height: 65px; 
 		display: flex;
 		margin-bottom: 1px;
@@ -64,7 +64,8 @@
 		align-items: center;
 		padding-right: 5px;
 		background-color: #fafafa;
-
+		white-space: nowrap;
+		
 		&:hover {
 			background-color: #eeeeee;
 		}

+ 1 - 1
im-ui/src/components/friend/FriendItem.vue

@@ -64,7 +64,7 @@
 		align-items: center;
 		padding-right: 5px;
 		background-color: #fafafa;
-
+		white-space: nowrap;
 		&:hover {
 			background-color: #eeeeee;
 		}

+ 1 - 1
im-ui/src/components/group/GroupItem.vue

@@ -42,7 +42,7 @@
 		align-items: center;
 		padding-right: 5px;
 		background-color: #fafafa;
-	
+		white-space: nowrap;
 		&:hover {
 			background-color: #eeeeee;
 		}

+ 68 - 35
im-ui/src/store/chatStore.js

@@ -1,21 +1,22 @@
 export default {
-	
+
 	state: {
 		activeIndex: -1,
 		chats: []
 	},
-	
+
 	mutations: {
 		initChatStore(state) {
 			state.activeIndex = -1;
 		},
-		openChat(state,chatInfo){
+		openChat(state, chatInfo) {
 			let chat = null;
-			for(let i in state.chats){
-				if(state.chats[i].targetId === chatInfo.targetId){
+			for (let i in state.chats) {
+				if (state.chats[i].type == chatInfo.type &&
+					state.chats[i].targetId === chatInfo.targetId) {
 					chat = state.chats[i];
 					// 放置头部
-					state.chats.splice(i,1);
+					state.chats.splice(i, 1);
 					state.chats.unshift(chat);
 					break;
 				}
@@ -35,55 +36,87 @@ export default {
 				state.chats.unshift(chat);
 			}
 
-		},	
-		activeChat(state,idx){
+		},
+		activeChat(state, idx) {
 			state.activeIndex = idx;
-			state.chats[idx].unreadCount=0;
+			state.chats[idx].unreadCount = 0;
 		},
-		removeChat(state,idx){
+		removeChat(state, idx) {
 			state.chats.splice(idx, 1);
-			if(state.activeIndex  >= state.chats.length){
-				state.activeIndex = state.chats.length-1;
+			if (state.activeIndex >= state.chats.length) {
+				state.activeIndex = state.chats.length - 1;
+			}
+		},
+		removeGroupChat(state, groupId) {
+			for (let idx in state.chats) {
+				if (state.chats[idx].type == 'GROUP' &&
+					state.chats[idx].targetId == groupId) {
+					this.commit("removeChat", idx);
+				}
+			}
+		},
+		removePrivateChat(state, userId) {
+			for (let idx in state.chats) {
+				if (state.chats[idx].type == 'PRIVATE' &&
+					state.chats[idx].targetId == userId) {
+					this.commit("removeChat", idx);
+				}
 			}
 		},
-		
 		insertMessage(state, msgInfo) {
-			let targetId = msgInfo.selfSend?msgInfo.recvId:msgInfo.sendId;
-			let chat = state.chats.find((chat)=>chat.targetId==targetId);
-		
-			chat.lastContent = msgInfo.content;
+			// 获取对方id或群id
+			let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
+			let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
+			let chat = null;
+			for (let idx in state.chats) {
+				if (state.chats[idx].type == type &&
+					state.chats[idx].targetId === targetId) {
+					chat = state.chats[idx];
+					break;
+				}
+			}
+			chat.lastContent = msgInfo.type == 1 ? "[图片]" : msgInfo.type == 2 ? "[文件]" : msgInfo.content;
 			chat.lastSendTime = msgInfo.sendTime;
 			chat.messages.push(msgInfo);
 			// 如果不是当前会话,未读加1
-			if(state.activeIndex == -1 || state.chats[state.activeIndex].targetId != targetId){
-				chat.unreadCount++;
+			chat.unreadCount++;
+			if(msgInfo.selfSend){
+				chat.unreadCount=0;
 			}
 		},
-		handleFileUpload(state,info){
+		handleFileUpload(state, info) {
 			// 文件上传后数据更新
-			let  chat = state.chats.find((c)=>c.targetId === info.targetId);
-			if(chat){
-				let msg = chat.messages.find((m)=>info.fileId==m.fileId);
-				msg.loadStatus = info.loadStatus;
-				if(info.content){
-					msg.content = info.content;
+			let chat = state.chats.find((c) => c.type==info.type && c.targetId === info.targetId);
+			let msg = chat.messages.find((m) => info.fileId == m.fileId);
+			msg.loadStatus = info.loadStatus;
+			if (info.content) {
+				msg.content = info.content;
+			}
+		},
+		updateChatFromUser(state, user) {
+			for (let i in state.chats) {
+				let chat = state.chats[i];
+				if (chat.type=='PRIVATE' && chat.targetId == user.id) {
+					chat.headImage = user.headImageThumb;
+					chat.showName = user.nickName;
+					break;
 				}
 			}
 		},
-		updateChatFromUser(state, user){
-			for(let i in state.chats){
-				if(state.chats[i].targetId == user.id){
-					state.chats[i].headImage = user.headImageThumb;
-					state.chats[i].showName = user.nickName;
+		updateChatFromGroup(state, group) {
+			for (let i in state.chats) {
+				let chat = state.chats[i];
+				if (chat.type=='GROUP' && chat.targetId == group.id) {
+					chat.headImage = group.headImageThumb;
+					chat.showName = group.remark;
 					break;
 				}
 			}
 		},
-		resetChatStore(state){
-			console.log("清空store")
+		resetChatStore(state) {
 			state.activeIndex = -1;
 			state.chats = [];
 		}
 	},
-	
-}
+
+}

+ 14 - 12
im-ui/src/view/Chat.vue

@@ -6,13 +6,14 @@
 					<el-button slot="append" icon="el-icon-search"></el-button>
 				</el-input>
 			</div>
-			<div v-for="(chat,index) in chatStore.chats" :key="chat.targetId">
+			<div v-for="(chat,index) in chatStore.chats" :key="chat.type+chat.targetId">
 				<chat-item :chat="chat" :index="index" @click.native="handleActiveItem(index)" @del="handleDelItem(chat,index)"
 				 :active="index === chatStore.activeIndex"></chat-item>
 			</div>
 		</el-aside>
 		<el-container class="r-chat-box">
-			<chat-private :chat="activeChat"></chat-private>
+			<chat-private :chat="activeChat" v-if="activeChat.type=='PRIVATE'"></chat-private>
+			<chat-Group :chat="activeChat" v-if="activeChat.type=='GROUP'"></chat-Group>
 		</el-container>
 	</el-container>
 </template>
@@ -24,7 +25,8 @@
 	import HeadImage from "../components/common/HeadImage.vue";
 	import FileUpload from "../components/common/FileUpload.vue";
 	import ChatPrivate from "../components/chat/ChatPrivate.vue";
-
+	import ChatGroup from "../components/chat/ChatGroup.vue";
+	
 	export default {
 		name: "chat",
 		components: {
@@ -33,25 +35,24 @@
 			HeadImage,
 			FileUpload,
 			MessageItem,
-			ChatPrivate
+			ChatPrivate,
+			ChatGroup
 		},
 		data() {
 			return {
 				searchText: "",
-				messageContent: ""
+				messageContent: "",
+				group: {},
+				groupMembers: [] 
 			}
 		},
 		methods: {
 			handleActiveItem(index) {
 				this.$store.commit("activeChat", index);
 				let chat = this.chatStore.chats[index];
-				if (chat.type == "GROUP") {
-					let groupId = this.chatStore.chats[index].targetId;
-
-				} else {
+				if (chat.type == "PRIVATE") {
 					this.refreshNameAndHeadImage(chat);
-				}
-
+				} 
 			},
 			handleDelItem(chat, index) {
 				this.$store.commit("removeChat", index);
@@ -107,7 +108,8 @@
 				}).then(() => {
 					this.$store.commit("updateFriend", friendInfo);
 				})
-			},
+			}
+			
 		},
 		computed: {
 			chatStore() {

+ 13 - 9
im-ui/src/view/Friend.vue

@@ -73,15 +73,19 @@
 				this.loadUserInfo(friend,index);
 			},
 			handleDelItem(friend, index) {
-				this.$http({
-					url: '/api/friend/delete',
-					method: 'delete',
-					params: {
-						friendId: friend.id
-					}
-				}).then((data) => {
-					this.$message.success("删除好友成功");
-					this.$store.commit("removeFriend", index);
+				this.$confirm(`确认要解除与 '${friend.nickName}'的好友关系吗?`, '确认解除?', {
+					confirmButtonText: '确定',
+					cancelButtonText: '取消',
+					type: 'warning'
+				}).then(() => {
+					this.$http({
+						url: `/api/friend/delete/${friend.id}`,
+						method: 'delete'
+					}).then((data) => {
+						this.$message.success("删除好友成功");
+						this.$store.commit("removeFriend", index);
+						this.$store.commit("removePrivateChat", friend.id);
+					})
 				})
 			},
 			handleSendMessage() {

+ 9 - 7
im-ui/src/view/Group.vue

@@ -26,7 +26,7 @@
 					<div class="r-group-info">
 						<div>
 							<file-upload class="avatar-uploader" action="/api/image/upload" :disabled="!isOwner" :showLoading="true"
-							 :maxSize="maxSize" @success="handleUploadSuccess" :fileTypes="['image/jpeg', 'image/png', 'image/jpg']">
+							 :maxSize="maxSize" @success="handleUploadSuccess" :fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
 								<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
 								<i v-else class="el-icon-plus avatar-uploader-icon"></i>
 							</file-upload>
@@ -34,19 +34,19 @@
 						</div>
 						<el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules" ref="groupForm">
 							<el-form-item label="群聊名称" prop="name">
-								<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="50"></el-input>
+								<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
 							</el-form-item>
 							<el-form-item label="群主">
-								<el-input :value="ownerName" disabled maxlength="50"></el-input>
+								<el-input :value="ownerName" disabled ></el-input>
 							</el-form-item>
 							<el-form-item label="备注">
-								<el-input v-model="activeGroup.remark" placeholder="群聊的备注仅自己可见"></el-input>
+								<el-input v-model="activeGroup.remark" placeholder="群聊的备注仅自己可见" maxlength="20"></el-input>
 							</el-form-item>
 							<el-form-item label="我在本群的昵称">
-								<el-input v-model="activeGroup.aliasName" placeholder=""></el-input>
+								<el-input v-model="activeGroup.aliasName" placeholder="" maxlength="20"></el-input>
 							</el-form-item>
 							<el-form-item label="群公告">
-								<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" placeholder="群主未设置"></el-input>
+								<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" maxlength="1024" placeholder="群主未设置"></el-input>
 							</el-form-item>
 							<div class="btn-group">
 								<el-button type="success" @click="handleSaveGroup()">提交</el-button>
@@ -172,6 +172,7 @@
 					}).then(() => {
 						this.$store.commit("removeGroup", this.groupStore.activeIndex);
 						this.$store.commit("activeGroup", -1);
+						this.$store.commit("removeGroupChat", this.activeGroup.id);
 					});
 				})
 
@@ -188,6 +189,7 @@
 					}).then(() => {
 						this.$store.commit("removeGroup", this.groupStore.activeIndex);
 						this.$store.commit("activeGroup", -1);
+						this.$store.commit("removeGroupChat", this.activeGroup.id);
 					});
 				})
 				
@@ -208,7 +210,7 @@
 					url: `/api/group/members/${this.activeGroup.id}`,
 					method: "get"
 				}).then((members) => {
-					this.groupMembers = members;
+					this.groupMembers = members.filter((m)=>!m.quit);
 				})
 			}
 		},

+ 73 - 10
im-ui/src/view/Home.vue

@@ -26,7 +26,7 @@
 						<span class="el-icon-setting"></span>
 				</el-menu-item>
 			</el-menu>
-			<div class="exit-box" @click="onExit()" title="退出">
+			<div class="exit-box" @click="handleExit()" title="退出">
 				<span class="el-icon-circle-close"></span>
 			</div>
 		</el-aside>
@@ -55,42 +55,105 @@
 				console.log("socket");
 				this.$wsApi.createWebSocket("ws://localhost:8878/im",this.$store);
 				this.$wsApi.onopen(()=>{
+					console.log("pullUnreadMessage")
 					this.pullUnreadMessage();
 				});
 				this.$wsApi.onmessage((e)=>{
 					console.log(e);
-					if(e.cmd==1){
+					if(e.cmd == 2){
+						// 异地登录,强制下线
+						this.$message.error("您已在其他地方登陆,将被强制下线");
+						setTimeout(()=>{
+							location.href="/";
+						},1000)
+						
+					}
+					else if(e.cmd==3){
 						// 插入私聊消息
 						this.handlePrivateMessage(e.data);
+					}else if(e.cmd == 4){
+						// 插入群聊消息
+						this.handleGroupMessage(e.data);
 					}
 				})
 			},
 			pullUnreadMessage(){
+				// 拉取未读私聊消息
 				this.$http({
 					url: "/api/message/private/pullUnreadMessage",
 					method: 'post'
-				})
+				});
+				// 拉取未读群聊消息
+				this.$http({
+					url: "/api/message/group/pullUnreadMessage",
+					method: 'post'
+				});
 			},
 			handlePrivateMessage(msg){
-				// 插入私聊消息
-				let f = this.$store.state.friendStore.friends.find((f)=>f.id==msg.sendId);
+				// 好友列表存在好友信息,直接插入私聊消息
+				let friend = this.$store.state.friendStore.friends.find((f)=>f.id==msg.sendId);
+				if(friend){
+					this.insertPrivateMessage(friend,msg);
+					return;
+				}
+				// 好友列表不存在好友信息,则发请求获取好友信息
+				this.$http({
+					url: `/api/friend/find/${msg.sendId}`,
+					method: 'get'
+				}).then((friend)=>{
+					this.insertPrivateMessage(friend,msg);
+					this.$store.commit("addFriend",friend);
+				})
+				
+				
+			},
+			insertPrivateMessage(friend,msg){
 				let chatInfo = {
 					type: 'PRIVATE',
-					targetId: f.id,
-					showName: f.nickName,
-					headImage: f.headImage
+					targetId: friend.id,
+					showName: friend.nickName,
+					headImage: friend.headImage
+				};
+				// 打开会话
+				this.$store.commit("openChat",chatInfo);
+				// 插入消息
+				this.$store.commit("insertMessage",msg);
+			},
+			handleGroupMessage(msg){
+				// 群聊缓存存在,直接插入群聊消息
+				let group = this.$store.state.groupStore.groups.find((g)=>g.id==msg.groupId);
+				if(group){
+					this.insertGroupMessage(group,msg);
+					return;
+				}
+				// 群聊缓存存在,直接插入群聊消息
+				this.$http({
+					url: `/api/group/find/${msg.groupId}`,
+					method: 'get'
+				}).then((group)=>{
+					this.insertGroupMessage(group,msg);
+					this.$store.commit("addGroup",group);
+				})
+			},
+			insertGroupMessage(group,msg){
+				let chatInfo = {
+					type: 'GROUP',
+					targetId: group.id,
+					showName: group.remark,
+					headImage: group.headImageThumb
 				};
 				// 打开会话
 				this.$store.commit("openChat",chatInfo);
 				// 插入消息
 				this.$store.commit("insertMessage",msg);
 			},
-			onExit(){
+			handleExit(){
 				this.$http({
 					url: "/api/logout",
 					method: 'get'
 				}).then(()=>{
-					this.$router.push("/login");
+					this.$wsApi.closeWebSocket();
+					location.href="/";
 				})
 			},
 			onClickHeadImage(){

+ 3 - 6
im-ui/src/view/Login.vue

@@ -1,10 +1,7 @@
 <template>
 	<div class="login-view">
-		
-			
-			
-			<el-form :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px" class="web-ruleForm">
-				<div class="login-brand">欢迎登陆fly-chat</div>
+			<el-form :model="loginForm"  status-icon :rules="rules" ref="loginForm" label-width="60px" class="web-ruleForm">
+				<div class="login-brand">欢迎登陆</div>
 				<el-form-item label="用户名" prop="username">
 					<el-input type="username" v-model="loginForm.username" autocomplete="off"></el-input>
 
@@ -95,7 +92,6 @@
 		height: 100%;
 		background:  linear-gradient(#65807a, #182e3c); 
 		background-size: cover;
-
 		
 		.web-ruleForm {
 			height: 340px;
@@ -114,6 +110,7 @@
 				font-weight: 600;
 				letter-spacing: 2px;
 				text-transform: uppercase;
+				text-align: center;
 			}
 			
 			.register {