Browse Source

群聊功能开发中

master
xie.bx 3 years ago
parent
commit
62e9c89121
  1. 2
      commom/src/main/java/com/lx/common/contant/RedisKey.java
  2. 8
      commom/src/main/java/com/lx/common/enums/WSCmdEnum.java
  3. 9
      commom/src/main/java/com/lx/common/model/im/LoginInfo.java
  4. 11
      im-platform/src/main/java/com/lx/implatform/controller/FriendController.java
  5. 10
      im-platform/src/main/java/com/lx/implatform/controller/GroupController.java
  6. 10
      im-platform/src/main/java/com/lx/implatform/controller/GroupMessageController.java
  7. 5
      im-platform/src/main/java/com/lx/implatform/entity/Group.java
  8. 7
      im-platform/src/main/java/com/lx/implatform/entity/GroupMember.java
  9. 2
      im-platform/src/main/java/com/lx/implatform/entity/GroupMessage.java
  10. 1
      im-platform/src/main/java/com/lx/implatform/service/IFriendService.java
  11. 2
      im-platform/src/main/java/com/lx/implatform/service/IGroupMemberService.java
  12. 4
      im-platform/src/main/java/com/lx/implatform/service/IGroupService.java
  13. 24
      im-platform/src/main/java/com/lx/implatform/service/impl/FriendServiceImpl.java
  14. 42
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMemberServiceImpl.java
  15. 50
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java
  16. 48
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java
  17. 3
      im-platform/src/main/java/com/lx/implatform/vo/GroupMemberVO.java
  18. 5
      im-platform/src/main/java/com/lx/implatform/vo/GroupVO.java
  19. 4
      im-platform/src/main/resources/db/db.sql
  20. 2
      im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadGroupMessageTask.java
  21. 8
      im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadPrivateMessageTask.java
  22. 11
      im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java
  23. 6
      im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java
  24. 9
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java
  25. 27
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java
  26. 64
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/LoginProcessor.java
  27. 12
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java
  28. 16
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/PrivateMessageProcessor.java
  29. 12
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java
  30. 30
      im-ui/src/api/wssocket.js
  31. 5
      im-ui/src/components/chat/ChatItem.vue
  32. 2
      im-ui/src/components/friend/FriendItem.vue
  33. 2
      im-ui/src/components/group/GroupItem.vue
  34. 61
      im-ui/src/store/chatStore.js
  35. 22
      im-ui/src/view/Chat.vue
  36. 14
      im-ui/src/view/Friend.vue
  37. 16
      im-ui/src/view/Group.vue
  38. 83
      im-ui/src/view/Home.vue
  39. 7
      im-ui/src/view/Login.vue

2
commom/src/main/java/com/lx/common/contant/RedisKey.java

@ -13,7 +13,7 @@ public class RedisKey {
// 已读私聊消息id队列 // 已读私聊消息id队列
public final static String IM_READED_PRIVATE_MESSAGE_ID = "im:readed:private:id"; public final static String IM_READED_PRIVATE_MESSAGE_ID = "im:readed:private:id";
// 已读群聊消息位置(已读最大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:"; public final static String IM_CACHE = "im:cache:";
// 缓存是否好友:bool // 缓存是否好友:bool

8
commom/src/main/java/com/lx/common/enums/WSCmdEnum.java

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

9
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;
}

11
im-platform/src/main/java/com/lx/implatform/controller/FriendController.java

@ -49,9 +49,16 @@ public class FriendController {
return ResultUtils.success(); 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="解除好友关系") @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); friendService.delFriend(friendId);
return ResultUtils.success(); return ResultUtils.success();
} }

10
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.Result;
import com.lx.common.result.ResultUtils; 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.service.IGroupService;
import com.lx.implatform.vo.GroupInviteVO; import com.lx.implatform.vo.GroupInviteVO;
import com.lx.implatform.vo.GroupMemberVO; import com.lx.implatform.vo.GroupMemberVO;
import com.lx.implatform.vo.GroupVO; import com.lx.implatform.vo.GroupVO;
import com.lx.implatform.vo.UserVO;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -15,6 +18,7 @@ import javax.validation.Valid;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@RestController @RestController
@ -43,6 +47,12 @@ public class GroupController {
return ResultUtils.success(); 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="查询群聊列表") @ApiOperation(value = "查询群聊列表",notes="查询群聊列表")
@GetMapping("/list") @GetMapping("/list")
public Result<List<GroupVO>> findGroups(){ public Result<List<GroupVO>> findGroups(){

10
im-platform/src/main/java/com/lx/implatform/controller/GroupMessageController.java

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

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

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

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

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

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

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

1
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); void update(FriendVO vo);
FriendVO findFriend(Long friendId);
} }

2
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 save(GroupMember member);
boolean saveBatch(Long groupId,List<GroupMember> members); boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members);
void removeByGroupId(Long groupId); void removeByGroupId(Long groupId);

4
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); void invite(GroupInviteVO vo);
Group findById(Long id); Group GetById(Long groupId);
GroupVO findById(Long groupId);
List<GroupMemberVO> findGroupMembers(Long groupId); List<GroupMemberVO> findGroupMembers(Long groupId);
} }

24
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.IFriendService;
import com.lx.implatform.service.IUserService; import com.lx.implatform.service.IUserService;
import com.lx.implatform.session.SessionContext; import com.lx.implatform.session.SessionContext;
import com.lx.implatform.session.UserSession;
import com.lx.implatform.vo.FriendVO; import com.lx.implatform.vo.FriendVO;
import org.springframework.aop.framework.AopContext; import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired; 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,"不允许添加自己为好友"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许添加自己为好友");
} }
// 互相绑定好友关系 // 互相绑定好友关系
bindFriend(userId,friendId); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
bindFriend(friendId,userId); proxy.bindFriend(userId,friendId);
proxy.bindFriend(friendId,userId);
} }
@ -99,6 +101,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
this.updateById(f); this.updateById(f);
} }
@CacheEvict(key="#userId+':'+#friendId")
public void bindFriend(Long userId, Long friendId) { public void bindFriend(Long userId, Long friendId) {
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); QueryWrapper<Friend> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda() 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;
}
} }

42
im-platform/src/main/java/com/lx/implatform/service/impl/GroupMemberServiceImpl.java

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

50
im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java

@ -1,10 +1,12 @@
package com.lx.implatform.service.impl; package com.lx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lx.common.contant.RedisKey; import com.lx.common.contant.RedisKey;
import com.lx.common.enums.ResultCode; import com.lx.common.enums.ResultCode;
import com.lx.common.model.im.GroupMessageInfo; import com.lx.common.model.im.GroupMessageInfo;
import com.lx.common.util.BeanUtils; import com.lx.common.util.BeanUtils;
import com.lx.implatform.entity.Group; import com.lx.implatform.entity.Group;
import com.lx.implatform.entity.GroupMember;
import com.lx.implatform.entity.GroupMessage; import com.lx.implatform.entity.GroupMessage;
import com.lx.implatform.exception.GlobalException; import com.lx.implatform.exception.GlobalException;
import com.lx.implatform.mapper.GroupMessageMapper; import com.lx.implatform.mapper.GroupMessageMapper;
@ -21,6 +23,7 @@ import org.springframework.stereotype.Service;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service @Service
@ -45,7 +48,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
@Override @Override
public void sendMessage(GroupMessageVO vo) { public void sendMessage(GroupMessageVO vo) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
Group group = groupService.findById(vo.getGroupId()); Group group = groupService.getById(vo.getGroupId());
if(group == null){ if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在或已解散"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在或已解散");
} }
@ -57,7 +60,15 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 根据群聊每个成员所连的IM-server,进行分组 // 根据群聊每个成员所连的IM-server,进行分组
Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>(); Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>();
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if(!userIds.contains(userId)){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法发送消息");
}
userIds.parallelStream().forEach(id->{ userIds.parallelStream().forEach(id->{
if(id == userId){
// 自己不需要推送给自己
return;
}
String key = RedisKey.IM_USER_SERVER_ID + id; String key = RedisKey.IM_USER_SERVER_ID + id;
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
if(serverId != null){ if(serverId != null){
@ -73,8 +84,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 逐个server发送 // 逐个server发送
for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) { for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) {
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
msgInfo.setRecvIds(entry.getValue()); msgInfo.setRecvIds(new LinkedList<>(entry.getValue()));
String key = RedisKey.IM_UNREAD_PRIVATE_MESSAGE +entry.getKey(); String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey();
redisTemplate.opsForList().rightPush(key,msgInfo); redisTemplate.opsForList().rightPush(key,msgInfo);
} }
} }
@ -86,6 +97,37 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
*/ */
@Override @Override
public void pullUnreadMessage() { 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());
}
} }
} }

48
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.lang.reflect.Member;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -126,12 +127,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if(group.getOwnerId() != session.getId()){ if(group.getOwnerId() != session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊");
} }
// 删除群数据 // 逻辑删除群数据
this.removeById(groupId); group.setDeleted(true);
// 删除成员数据 this.updateById(group);
groupMemberService.removeByGroupId(groupId);
} }
/** /**
*退出群聊 *退出群聊
* *
@ -152,6 +153,24 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
groupMemberService.removeByGroupAndUserId(groupId,session.getId()); 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查找群聊并进行缓存 *根据id查找群聊并进行缓存
* *
@ -160,10 +179,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
*/ */
@Cacheable(value = "#groupId") @Cacheable(value = "#groupId")
@Override @Override
public Group findById(Long groupId){ public Group GetById(Long groupId){
return super.getById(groupId); return super.getById(groupId);
} }
/** /**
* 查询当前用户的所有群聊 * 查询当前用户的所有群聊
* *
@ -208,16 +229,11 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
} }
// 群聊人数校验 // 群聊人数校验
List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId()); 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+"人"); 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> friends = friendsService.findFriendByUserId(session.getId());
List<Friend> friendsList = vo.getFriendIds().stream().map(id -> 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() List<GroupMember> groupMembers = friendsList.stream()
.map(f -> { .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.setGroupId(vo.getGroupId());
groupMember.setUserId(f.getFriendId()); groupMember.setUserId(f.getFriendId());
groupMember.setAliasName(f.getFriendNickName()); groupMember.setAliasName(f.getFriendNickName());
groupMember.setRemark(group.getName()); groupMember.setRemark(group.getName());
groupMember.setHeadImage(f.getFriendHeadImage()); groupMember.setHeadImage(f.getFriendHeadImage());
groupMember.setQuit(false);
return groupMember; return groupMember;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
if(!groupMembers.isEmpty()) { if(!groupMembers.isEmpty()) {
groupMemberService.saveBatch(group.getId(),groupMembers); groupMemberService.saveOrUpdateBatch(group.getId(),groupMembers);
} }
} }

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

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

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

4
im-platform/src/main/resources/db/db.sql

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

2
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){ for(Object o: messageInfos){
redisTemplate.opsForList().leftPop(key); redisTemplate.opsForList().leftPop(key);
GroupMessageInfo messageInfo = (GroupMessageInfo)o; GroupMessageInfo messageInfo = (GroupMessageInfo)o;
MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.PRIVATE_MESSAGE); MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.GROUP_MESSAGE);
processor.process(null,messageInfo); processor.process(null,messageInfo);
} }
} }

8
im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadPrivateMessageTask.java

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

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

@ -31,10 +31,8 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<SendInfo> {
@Override @Override
protected void channelRead0(ChannelHandlerContext ctx, SendInfo sendInfo) throws Exception { 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())); 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 @Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID"); AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID");
Long userId = ctx.channel().attr(attr).get(); Long userId = ctx.channel().attr(attr).get();
ChannelHandlerContext context = WebsocketChannelCtxHloder.getChannelCtx(userId);
// 判断一下,避免异地登录导致的误删
if(context != null && ctx.channel().id().equals(context.channel().id())){
// 移除channel // 移除channel
WebsocketChannelCtxHloder.removeChannelCtx(userId); WebsocketChannelCtxHloder.removeChannelCtx(userId);
// 用户下线 // 用户下线
RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate"); RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
String key = RedisKey.IM_USER_SERVER_ID + userId; String key = RedisKey.IM_USER_SERVER_ID + userId;
redisTemplate.delete(key); redisTemplate.delete(key);
log.info(ctx.channel().id().asLongText() + "断开连接"); log.info("断开连接,userId:{}",userId);
}
} }
@Override @Override

6
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 io.netty.channel.ChannelHandlerContext;
import java.util.Map; import java.util.*;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class WebsocketChannelCtxHloder { public class WebsocketChannelCtxHloder {
@ -19,6 +18,9 @@ public class WebsocketChannelCtxHloder {
channelMap.remove(userId); channelMap.remove(userId);
} }
public static ChannelHandlerContext getChannelCtx(Long userId){ public static ChannelHandlerContext getChannelCtx(Long userId){
return channelMap.get(userId); return channelMap.get(userId);
} }

9
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java

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

27
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java

@ -1,25 +1,21 @@
package com.lx.implatform.imserver.websocket.processor; package com.lx.implatform.imserver.websocket.processor;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.lx.common.contant.RedisKey;
import com.lx.common.enums.WSCmdEnum; import com.lx.common.enums.WSCmdEnum;
import com.lx.common.model.im.HeartbeatInfo; import com.lx.common.model.im.HeartbeatInfo;
import com.lx.common.model.im.SendInfo; import com.lx.common.model.im.SendInfo;
import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder;
import com.lx.implatform.imserver.websocket.WebsocketServer; import com.lx.implatform.imserver.websocket.WebsocketServer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Component @Component
public class HeartbeatProcessor implements MessageProcessor<HeartbeatInfo> { public class HeartbeatProcessor extends MessageProcessor<HeartbeatInfo> {
@Autowired @Autowired
@ -30,22 +26,17 @@ public class HeartbeatProcessor implements MessageProcessor<HeartbeatInfo> {
@Override @Override
public void process(ChannelHandlerContext ctx, HeartbeatInfo beatInfo) { 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 // 响应ws
SendInfo sendInfo = new SendInfo(); SendInfo sendInfo = new SendInfo();
sendInfo.setCmd(WSCmdEnum.HEARTBEAT.getCode()); sendInfo.setCmd(WSCmdEnum.HEART_BEAT.getCode());
ctx.channel().writeAndFlush(sendInfo); 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
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;
}
}

12
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java

@ -1,9 +1,17 @@
package com.lx.implatform.imserver.websocket.processor; package com.lx.implatform.imserver.websocket.processor;
import io.netty.channel.ChannelHandlerContext; 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);
} }

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

12
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){ public static MessageProcessor createProcessor(WSCmdEnum cmd){
MessageProcessor processor = null; MessageProcessor processor = null;
switch (cmd){ switch (cmd){
case HEARTBEAT: case LOGIN:
processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean("heartbeatProcessor"); processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class);
break;
case HEART_BEAT:
processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class);
break; break;
case PRIVATE_MESSAGE: 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; break;
default: default:
break; break;

30
im-ui/src/api/wssocket.js

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

5
im-ui/src/components/chat/ChatItem.vue

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

2
im-ui/src/components/friend/FriendItem.vue

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

2
im-ui/src/components/group/GroupItem.vue

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

61
im-ui/src/store/chatStore.js

@ -12,7 +12,8 @@ export default {
openChat(state, chatInfo) { openChat(state, chatInfo) {
let chat = null; let chat = null;
for (let i in state.chats) { for (let i in state.chats) {
if(state.chats[i].targetId === chatInfo.targetId){ if (state.chats[i].type == chatInfo.type &&
state.chats[i].targetId === chatInfo.targetId) {
chat = state.chats[i]; chat = state.chats[i];
// 放置头部 // 放置头部
state.chats.splice(i, 1); state.chats.splice(i, 1);
@ -46,41 +47,73 @@ export default {
state.activeIndex = state.chats.length - 1; 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) { insertMessage(state, msgInfo) {
let targetId = msgInfo.selfSend?msgInfo.recvId:msgInfo.sendId; // 获取对方id或群id
let chat = state.chats.find((chat)=>chat.targetId==targetId); let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
chat.lastContent = msgInfo.content; 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.lastSendTime = msgInfo.sendTime;
chat.messages.push(msgInfo); chat.messages.push(msgInfo);
// 如果不是当前会话,未读加1 // 如果不是当前会话,未读加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); let chat = state.chats.find((c) => c.type==info.type && c.targetId === info.targetId);
if(chat){
let msg = chat.messages.find((m) => info.fileId == m.fileId); let msg = chat.messages.find((m) => info.fileId == m.fileId);
msg.loadStatus = info.loadStatus; msg.loadStatus = info.loadStatus;
if (info.content) { if (info.content) {
msg.content = info.content; msg.content = info.content;
} }
}
}, },
updateChatFromUser(state, user) { updateChatFromUser(state, user) {
for (let i in state.chats) { for (let i in state.chats) {
if(state.chats[i].targetId == user.id){ let chat = state.chats[i];
state.chats[i].headImage = user.headImageThumb; if (chat.type=='PRIVATE' && chat.targetId == user.id) {
state.chats[i].showName = user.nickName; chat.headImage = user.headImageThumb;
chat.showName = user.nickName;
break;
}
}
},
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; break;
} }
} }
}, },
resetChatStore(state) { resetChatStore(state) {
console.log("清空store")
state.activeIndex = -1; state.activeIndex = -1;
state.chats = []; state.chats = [];
} }

22
im-ui/src/view/Chat.vue

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

14
im-ui/src/view/Friend.vue

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

16
im-ui/src/view/Group.vue

@ -26,7 +26,7 @@
<div class="r-group-info"> <div class="r-group-info">
<div> <div>
<file-upload class="avatar-uploader" action="/api/image/upload" :disabled="!isOwner" :showLoading="true" <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"> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload> </file-upload>
@ -34,19 +34,19 @@
</div> </div>
<el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules" ref="groupForm"> <el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules" ref="groupForm">
<el-form-item label="群聊名称" prop="name"> <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>
<el-form-item label="群主"> <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>
<el-form-item label="备注"> <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>
<el-form-item label="我在本群的昵称"> <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>
<el-form-item label="群公告"> <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> </el-form-item>
<div class="btn-group"> <div class="btn-group">
<el-button type="success" @click="handleSaveGroup()">提交</el-button> <el-button type="success" @click="handleSaveGroup()">提交</el-button>
@ -172,6 +172,7 @@
}).then(() => { }).then(() => {
this.$store.commit("removeGroup", this.groupStore.activeIndex); this.$store.commit("removeGroup", this.groupStore.activeIndex);
this.$store.commit("activeGroup", -1); this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.activeGroup.id);
}); });
}) })
@ -188,6 +189,7 @@
}).then(() => { }).then(() => {
this.$store.commit("removeGroup", this.groupStore.activeIndex); this.$store.commit("removeGroup", this.groupStore.activeIndex);
this.$store.commit("activeGroup", -1); this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.activeGroup.id);
}); });
}) })
@ -208,7 +210,7 @@
url: `/api/group/members/${this.activeGroup.id}`, url: `/api/group/members/${this.activeGroup.id}`,
method: "get" method: "get"
}).then((members) => { }).then((members) => {
this.groupMembers = members; this.groupMembers = members.filter((m)=>!m.quit);
}) })
} }
}, },

83
im-ui/src/view/Home.vue

@ -26,7 +26,7 @@
<span class="el-icon-setting"></span> <span class="el-icon-setting"></span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="exit-box" @click="onExit()" title="退出"> <div class="exit-box" @click="handleExit()" title="退出">
<span class="el-icon-circle-close"></span> <span class="el-icon-circle-close"></span>
</div> </div>
</el-aside> </el-aside>
@ -55,42 +55,105 @@
console.log("socket"); console.log("socket");
this.$wsApi.createWebSocket("ws://localhost:8878/im",this.$store); this.$wsApi.createWebSocket("ws://localhost:8878/im",this.$store);
this.$wsApi.onopen(()=>{ this.$wsApi.onopen(()=>{
console.log("pullUnreadMessage")
this.pullUnreadMessage(); this.pullUnreadMessage();
}); });
this.$wsApi.onmessage((e)=>{ this.$wsApi.onmessage((e)=>{
console.log(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); this.handlePrivateMessage(e.data);
}else if(e.cmd == 4){
//
this.handleGroupMessage(e.data);
} }
}) })
}, },
pullUnreadMessage(){ pullUnreadMessage(){
//
this.$http({ this.$http({
url: "/api/message/private/pullUnreadMessage", url: "/api/message/private/pullUnreadMessage",
method: 'post' method: 'post'
}) });
//
this.$http({
url: "/api/message/group/pullUnreadMessage",
method: 'post'
});
}, },
handlePrivateMessage(msg){ 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 = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: f.id, targetId: friend.id,
showName: f.nickName, showName: friend.nickName,
headImage: f.headImage 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("openChat",chatInfo);
// //
this.$store.commit("insertMessage",msg); this.$store.commit("insertMessage",msg);
}, },
onExit(){ handleExit(){
this.$http({ this.$http({
url: "/api/logout", url: "/api/logout",
method: 'get' method: 'get'
}).then(()=>{ }).then(()=>{
this.$router.push("/login"); this.$wsApi.closeWebSocket();
location.href="/";
}) })
}, },
onClickHeadImage(){ onClickHeadImage(){

7
im-ui/src/view/Login.vue

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

Loading…
Cancel
Save