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. 23
      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. 28
      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. 34
      im-ui/src/api/wssocket.js
  31. 7
      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. 103
      im-ui/src/store/chatStore.js
  35. 26
      im-ui/src/view/Chat.vue
  36. 22
      im-ui/src/view/Friend.vue
  37. 16
      im-ui/src/view/Group.vue
  38. 83
      im-ui/src/view/Home.vue
  39. 9
      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队列
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

8
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
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();
}
@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
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(){

10
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
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
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;
/**
* 创建时间
*/

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:文件
*/
@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);
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 saveBatch(Long groupId,List<GroupMember> members);
boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members);
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);
Group findById(Long id);
Group GetById(Long groupId);
GroupVO findById(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.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;
}
}

42
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);
}
}

50
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());
}
}
}

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.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
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
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;

4
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 '发送时间',

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){
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);
}
}

8
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);
}
}

23
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

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

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

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

28
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());
}
}

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){
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;

34
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();
}
}

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

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

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

103
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 = [];
}
},
}
}

26
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() {

22
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() {

16
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);
})
}
},

83
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(){

9
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 {

Loading…
Cancel
Save