Browse Source

支持查询聊天记录

master
xie.bx 3 years ago
parent
commit
0058391a4d
  1. 5
      commom/pom.xml
  2. 3
      commom/src/main/java/com/bx/common/model/im/GroupMessageInfo.java
  3. 3
      commom/src/main/java/com/bx/common/model/im/PrivateMessageInfo.java
  4. 28
      commom/src/main/java/com/bx/common/serializer/DateToLongSerializer.java
  5. 9
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  6. 12
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  7. 5
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  8. 5
      im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java
  9. 4
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  10. 38
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  11. 14
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  12. 103
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  13. 4
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  14. 8
      im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java
  15. 2
      im-platform/src/main/java/com/bx/implatform/util/FileUtil.java
  16. 2
      im-platform/src/main/resources/application.yml
  17. 1
      im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java
  18. 21
      im-ui/src/components/chat/ChatBox.vue
  19. 170
      im-ui/src/components/chat/ChatHistory.vue
  20. 6
      im-ui/src/components/chat/MessageItem.vue

5
commom/pom.xml

@ -58,5 +58,10 @@
<artifactId>velocity</artifactId> <artifactId>velocity</artifactId>
<version>${velocity.version}</version> <version>${velocity.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.10</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

3
commom/src/main/java/com/bx/common/model/im/GroupMessageInfo.java

@ -1,5 +1,7 @@
package com.bx.common.model.im; package com.bx.common.model.im;
import com.bx.common.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
@ -20,5 +22,6 @@ public class GroupMessageInfo {
private Integer type; private Integer type;
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime; private Date sendTime;
} }

3
commom/src/main/java/com/bx/common/model/im/PrivateMessageInfo.java

@ -1,5 +1,7 @@
package com.bx.common.model.im; package com.bx.common.model.im;
import com.bx.common.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
@ -17,5 +19,6 @@ public class PrivateMessageInfo {
private Integer type; private Integer type;
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime; private Date sendTime;
} }

28
commom/src/main/java/com/bx/common/serializer/DateToLongSerializer.java

@ -0,0 +1,28 @@
package com.bx.common.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import java.io.IOException;
import java.util.Date;
public class DateToLongSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(date.getTime());
}
@Override
public void serializeWithType(Date value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen,
typeSer.typeId(value, JsonToken.VALUE_STRING));
serialize(value, gen, serializers);
typeSer.writeTypeSuffix(gen, typeIdDef);
}
}

9
im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java

@ -1,6 +1,7 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.common.model.im.GroupMessageInfo;
import com.bx.common.result.Result; import com.bx.common.result.Result;
import com.bx.common.result.ResultUtils; import com.bx.common.result.ResultUtils;
import com.bx.implatform.service.IGroupMessageService; import com.bx.implatform.service.IGroupMessageService;
@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
@Api(tags = "群聊消息") @Api(tags = "群聊消息")
@ -43,5 +45,12 @@ public class GroupMessageController {
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<GroupMessageInfo>> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId,
@NotNull(message = "页码不能为空") @RequestParam Long page,
@NotNull(message = "size不能为空") @RequestParam Long size){
return ResultUtils.success( groupMessageService.findHistoryMessage(groupId,page,size));
}
} }

12
im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java

@ -1,6 +1,7 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.common.model.im.PrivateMessageInfo;
import com.bx.common.result.Result; import com.bx.common.result.Result;
import com.bx.common.result.ResultUtils; import com.bx.common.result.ResultUtils;
import com.bx.implatform.service.IPrivateMessageService; import com.bx.implatform.service.IPrivateMessageService;
@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
@Api(tags = "私聊消息") @Api(tags = "私聊消息")
@RestController @RestController
@ -42,5 +44,15 @@ public class PrivateMessageController {
privateMessageService.pullUnreadMessage(); privateMessageService.pullUnreadMessage();
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<PrivateMessageInfo>> recallMessage(@NotNull(message = "好友id不能为空") @RequestParam Long friendId,
@NotNull(message = "页码不能为空") @RequestParam Long page,
@NotNull(message = "size不能为空") @RequestParam Long size){
return ResultUtils.success( privateMessageService.findHistoryMessage(friendId,page,size));
}
} }

5
im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java

@ -1,9 +1,12 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.common.model.im.GroupMessageInfo;
import com.bx.implatform.entity.GroupMessage; import com.bx.implatform.entity.GroupMessage;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupMessageVO;
import java.util.List;
public interface IGroupMessageService extends IService<GroupMessage> { public interface IGroupMessageService extends IService<GroupMessage> {
@ -13,4 +16,6 @@ public interface IGroupMessageService extends IService<GroupMessage> {
void recallMessage(Long id); void recallMessage(Long id);
void pullUnreadMessage(); void pullUnreadMessage();
List<GroupMessageInfo> findHistoryMessage(Long groupId, Long page, Long size);
} }

5
im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java

@ -1,9 +1,12 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.common.model.im.PrivateMessageInfo;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.vo.PrivateMessageVO; import com.bx.implatform.vo.PrivateMessageVO;
import java.util.List;
public interface IPrivateMessageService extends IService<PrivateMessage> { public interface IPrivateMessageService extends IService<PrivateMessage> {
@ -11,6 +14,8 @@ public interface IPrivateMessageService extends IService<PrivateMessage> {
void recallMessage(Long id); void recallMessage(Long id);
List<PrivateMessageInfo> findHistoryMessage(Long friendId, Long page,Long size);
void pullUnreadMessage(); void pullUnreadMessage();
} }

4
im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java

@ -13,6 +13,7 @@ import com.bx.implatform.service.IUserService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.vo.FriendVO; import com.bx.implatform.vo.FriendVO;
import lombok.extern.slf4j.Slf4j;
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;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
@ -24,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@Slf4j
@CacheConfig(cacheNames= RedisKey.IM_CACHE_FRIEND) @CacheConfig(cacheNames= RedisKey.IM_CACHE_FRIEND)
@Service @Service
public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> implements IFriendService { public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> implements IFriendService {
@ -63,6 +65,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy(); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.bindFriend(userId,friendId); proxy.bindFriend(userId,friendId);
proxy.bindFriend(friendId,userId); proxy.bindFriend(friendId,userId);
log.info("添加好友,用户id:{},好友id:{}",userId,friendId);
} }
@ -80,6 +83,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy(); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.unbindFriend(userId,friendId); proxy.unbindFriend(userId,friendId);
proxy.unbindFriend(friendId,userId); proxy.unbindFriend(friendId,userId);
log.info("删除好友,用户id:{},好友id:{}",userId,friendId);
} }

38
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@ -46,7 +46,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
* 发送群聊消息(与mysql所有交换都要进行缓存) * 发送群聊消息(与mysql所有交换都要进行缓存)
* *
* @param vo * @param vo
* @return * @return 群聊id
*/ */
@Override @Override
public Long sendMessage(GroupMessageVO vo) { public Long sendMessage(GroupMessageVO vo) {
@ -161,6 +161,42 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
/**
* 拉取历史聊天记录
*
* @param groupId 群聊id
* @param page 页码
* @param size 页码大小
* @return 聊天记录列表
*/
@Override
public List<GroupMessageInfo> findHistoryMessage(Long groupId, Long page, Long size) {
page = page > 0 ? page:1;
size = size > 0 ? size:10;
Long userId = SessionContext.getSession().getId();
Long stIdx = (page-1)* size;
// 群聊成员信息
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,userId);
if(member == null || member.getQuit()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊中");
}
// 查询聊天记录,只查询加入群聊时间之后的消息
QueryWrapper<GroupMessage> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(GroupMessage::getGroupId,groupId)
.gt(GroupMessage::getSendTime,member.getCreatedTime())
.ne(GroupMessage::getStatus,MessageStatusEnum.RECALL.getCode())
.orderByDesc(GroupMessage::getId)
.last("limit "+stIdx + ","+size);
List<GroupMessage> messages = this.list(wrapper);
List<GroupMessageInfo> messageInfos = messages.stream().map(m->{
GroupMessageInfo info = BeanUtils.copyProperties(m, GroupMessageInfo.class);
return info;
}).collect(Collectors.toList());
log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}",userId,groupId,messageInfos.size());
return messageInfos;
}
private void sendMessage(List<Long> userIds, GroupMessageInfo msgInfo){ private void sendMessage(List<Long> userIds, GroupMessageInfo msgInfo){
// 根据群聊每个成员所连的IM-server,进行分组 // 根据群聊每个成员所连的IM-server,进行分组
Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>(); Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>();

14
im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java

@ -21,6 +21,7 @@ import com.bx.implatform.session.UserSession;
import com.bx.implatform.vo.GroupInviteVO; import com.bx.implatform.vo.GroupInviteVO;
import com.bx.implatform.vo.GroupMemberVO; import com.bx.implatform.vo.GroupMemberVO;
import com.bx.implatform.vo.GroupVO; import com.bx.implatform.vo.GroupVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
@ -36,6 +37,7 @@ import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
@CacheConfig(cacheNames = RedisKey.IM_CACHE_GROUP) @CacheConfig(cacheNames = RedisKey.IM_CACHE_GROUP)
@Service @Service
public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements IGroupService { public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements IGroupService {
@ -79,6 +81,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class); GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class);
vo.setAliasName(user.getNickName()); vo.setAliasName(user.getNickName());
vo.setRemark(groupName); vo.setRemark(groupName);
log.info("创建群聊,群聊id:{},群聊名称:{}",group.getId(),group.getName());
return vo; return vo;
} }
@ -109,6 +112,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
member.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName()); member.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName());
member.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark()); member.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark());
groupMemberService.updateById(member); groupMemberService.updateById(member);
log.info("修改群聊,群聊id:{},群聊名称:{}",group.getId(),group.getName());
return vo; return vo;
} }
@ -131,6 +135,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 逻辑删除群数据 // 逻辑删除群数据
group.setDeleted(true); group.setDeleted(true);
this.updateById(group); this.updateById(group);
log.info("删除群聊,群聊id:{},群聊名称:{}",group.getId(),group.getName());
} }
@ -142,13 +147,14 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
*/ */
@Override @Override
public void quitGroup(Long groupId) { public void quitGroup(Long groupId) {
UserSession session = SessionContext.getSession(); Long userId = SessionContext.getSession().getId();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(group.getOwnerId() == session.getId()){ if(group.getOwnerId() == userId){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊");
} }
// 删除群聊成员 // 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId,session.getId()); groupMemberService.removeByGroupAndUserId(groupId,userId);
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}",group.getId(),group.getName(),userId);
} }
@ -171,6 +177,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
} }
// 删除群聊成员 // 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId,userId); groupMemberService.removeByGroupAndUserId(groupId,userId);
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}",group.getId(),group.getName(),userId);
} }
@Override @Override
@ -281,6 +288,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if(!groupMembers.isEmpty()) { if(!groupMembers.isEmpty()) {
groupMemberService.saveOrUpdateBatch(group.getId(),groupMembers); groupMemberService.saveOrUpdateBatch(group.getId(),groupMembers);
} }
log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}",group.getId(),group.getName(),vo.getFriendIds());
} }
/** /**

103
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -32,7 +32,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Autowired @Autowired
private IFriendService friendService; private IFriendService friendService;
@Autowired @Autowired
private RedisTemplate<String, Object> redisTemplate; private RedisTemplate<String, Object> redisTemplate;
/** /**
* 发送私聊消息 * 发送私聊消息
@ -43,9 +43,9 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Override @Override
public Long sendMessage(PrivateMessageVO vo) { public Long sendMessage(PrivateMessageVO vo) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
Boolean isFriends = friendService.isFriend(userId,vo.getRecvId()); Boolean isFriends = friendService.isFriend(userId, vo.getRecvId());
if(!isFriends){ if (!isFriends) {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不是对方好友,无法发送消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不是对方好友,无法发送消息");
} }
// 保存消息 // 保存消息
PrivateMessage msg = BeanUtils.copyProperties(vo, PrivateMessage.class); PrivateMessage msg = BeanUtils.copyProperties(vo, PrivateMessage.class);
@ -54,15 +54,15 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
msg.setSendTime(new Date()); msg.setSendTime(new Date());
this.save(msg); this.save(msg);
// 获取对方连接的channelId // 获取对方连接的channelId
String key = RedisKey.IM_USER_SERVER_ID+msg.getRecvId(); String key = RedisKey.IM_USER_SERVER_ID + msg.getRecvId();
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); Integer serverId = (Integer) redisTemplate.opsForValue().get(key);
// 如果对方在线,将数据存储至redis,等待拉取推送 // 如果对方在线,将数据存储至redis,等待拉取推送
if(serverId != null){ if (serverId != null) {
String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId; String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId;
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class);
redisTemplate.opsForList().rightPush(sendKey,msgInfo); redisTemplate.opsForList().rightPush(sendKey, msgInfo);
} }
log.info("发送私聊消息,发送id:{},接收id:{},内容:{}",userId,vo.getRecvId(),vo.getContent()); log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", userId, vo.getRecvId(), vo.getContent());
return msg.getId(); return msg.getId();
} }
@ -75,31 +75,66 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
public void recallMessage(Long id) { public void recallMessage(Long id) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
PrivateMessage msg = this.getById(id); PrivateMessage msg = this.getById(id);
if(msg == null){ if (msg == null) {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息不存在");
} }
if(msg.getSendId() != userId){ if (msg.getSendId() != userId) {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"这条消息不是由您发送,无法撤回"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "这条消息不是由您发送,无法撤回");
} }
if(System.currentTimeMillis() - msg.getSendTime().getTime() > Constant.ALLOW_RECALL_SECOND * 1000){ if (System.currentTimeMillis() - msg.getSendTime().getTime() > Constant.ALLOW_RECALL_SECOND * 1000) {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息已发送超过5分钟,无法撤回");
} }
// 修改消息状态 // 修改消息状态
msg.setStatus(MessageStatusEnum.RECALL.getCode()); msg.setStatus(MessageStatusEnum.RECALL.getCode());
this.updateById(msg); this.updateById(msg);
// 获取对方连接的channelId // 获取对方连接的channelId
String key = RedisKey.IM_USER_SERVER_ID+msg.getRecvId(); String key = RedisKey.IM_USER_SERVER_ID + msg.getRecvId();
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); Integer serverId = (Integer) redisTemplate.opsForValue().get(key);
// 如果对方在线,将数据存储至redis,等待拉取推送 // 如果对方在线,将数据存储至redis,等待拉取推送
if(serverId != null){ if (serverId != null) {
String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId; String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId;
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class);
msgInfo.setType(MessageTypeEnum.TIP.getCode()); msgInfo.setType(MessageTypeEnum.TIP.getCode());
msgInfo.setSendTime(new Date()); msgInfo.setSendTime(new Date());
msgInfo.setContent("对方撤回了一条消息"); msgInfo.setContent("对方撤回了一条消息");
redisTemplate.opsForList().rightPush(sendKey,msgInfo); redisTemplate.opsForList().rightPush(sendKey, msgInfo);
} }
log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}",msg.getSendId(),msg.getRecvId(),msg.getContent()); log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent());
}
/**
* 拉取历史聊天记录
*
* @param friendId 好友id
* @param page 页码
* @param size 页码大小
* @return 聊天记录列表
*/
@Override
public List<PrivateMessageInfo> findHistoryMessage(Long friendId, Long page, Long size) {
page = page > 0 ? page : 1;
size = size > 0 ? size : 10;
Long userId = SessionContext.getSession().getId();
Long stIdx = (page - 1) * size;
QueryWrapper<PrivateMessage> wrapper = new QueryWrapper<>();
wrapper.lambda().and(wrap -> wrap.and(
wp -> wp.eq(PrivateMessage::getSendId, userId)
.eq(PrivateMessage::getRecvId, friendId))
.or(wp -> wp.eq(PrivateMessage::getRecvId, userId)
.eq(PrivateMessage::getSendId, friendId)))
.ne(PrivateMessage::getStatus, MessageStatusEnum.RECALL.getCode())
.orderByDesc(PrivateMessage::getId)
.last("limit " + stIdx + "," + size);
List<PrivateMessage> messages = this.list(wrapper);
List<PrivateMessageInfo> messageInfos = messages.stream().map(m -> {
PrivateMessageInfo info = BeanUtils.copyProperties(m, PrivateMessageInfo.class);
return info;
}).collect(Collectors.toList());
log.info("拉取聊天记录,用户id:{},好友id:{},数量:{}", userId, friendId, messageInfos.size());
return messageInfos;
} }
/** /**
@ -111,25 +146,25 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
public void pullUnreadMessage() { public void pullUnreadMessage() {
// 获取当前连接的channelId // 获取当前连接的channelId
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
String key = RedisKey.IM_USER_SERVER_ID+userId; String key = RedisKey.IM_USER_SERVER_ID + userId;
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); Integer serverId = (Integer) redisTemplate.opsForValue().get(key);
if(serverId == null){ if (serverId == null) {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"用户未建立连接"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "用户未建立连接");
} }
// 获取当前用户所有未读消息 // 获取当前用户所有未读消息
QueryWrapper<PrivateMessage> queryWrapper = new QueryWrapper<>(); QueryWrapper<PrivateMessage> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(PrivateMessage::getRecvId,userId) queryWrapper.lambda().eq(PrivateMessage::getRecvId, userId)
.eq(PrivateMessage::getStatus,MessageStatusEnum.UNREAD); .eq(PrivateMessage::getStatus, MessageStatusEnum.UNREAD);
List<PrivateMessage> messages = this.list(queryWrapper); List<PrivateMessage> messages = this.list(queryWrapper);
// 上传至redis,等待推送 // 上传至redis,等待推送
if(!messages.isEmpty()){ if (!messages.isEmpty()) {
List<PrivateMessageInfo> infos = messages.stream().map(m->{ List<PrivateMessageInfo> infos = messages.stream().map(m -> {
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(m, PrivateMessageInfo.class); PrivateMessageInfo msgInfo = BeanUtils.copyProperties(m, PrivateMessageInfo.class);
return msgInfo; return msgInfo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId; String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId;
redisTemplate.opsForList().rightPushAll(sendKey,infos.toArray()); redisTemplate.opsForList().rightPushAll(sendKey, infos.toArray());
log.info("拉取未读私聊消息,用户id:{},数量:{}",userId,infos.size()); log.info("拉取未读私聊消息,用户id:{},数量:{}", userId, infos.size());
} }
} }
} }

4
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -17,6 +17,7 @@ import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.vo.RegisterVO; import com.bx.implatform.vo.RegisterVO;
import com.bx.implatform.vo.UserVO; import com.bx.implatform.vo.UserVO;
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.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@ -28,6 +29,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
@Service @Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@ -58,6 +60,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
user = BeanUtils.copyProperties(vo,User.class); user = BeanUtils.copyProperties(vo,User.class);
user.setPassword(passwordEncoder.encode(user.getPassword())); user.setPassword(passwordEncoder.encode(user.getPassword()));
this.save(user); this.save(user);
log.info("注册用户,用户id:{},用户名:{},昵称:{}",user.getId(),vo.getUserName(),vo.getNickName());
} }
/** /**
@ -116,6 +119,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
user.setHeadImage(vo.getHeadImage()); user.setHeadImage(vo.getHeadImage());
user.setHeadImageThumb(vo.getHeadImageThumb()); user.setHeadImageThumb(vo.getHeadImageThumb());
this.updateById(user); this.updateById(user);
log.info("用户信息更新,用户:{}}",user.toString());
} }

8
im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java

@ -4,6 +4,7 @@ import com.bx.common.contant.Constant;
import com.bx.common.enums.FileTypeEnum; import com.bx.common.enums.FileTypeEnum;
import com.bx.common.enums.ResultCode; import com.bx.common.enums.ResultCode;
import com.bx.implatform.exception.GlobalException; import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.session.SessionContext;
import com.bx.implatform.util.FileUtil; import com.bx.implatform.util.FileUtil;
import com.bx.implatform.util.ImageUtil; import com.bx.implatform.util.ImageUtil;
import com.bx.implatform.util.MinioUtil; import com.bx.implatform.util.MinioUtil;
@ -50,6 +51,7 @@ public class FileService {
public String uploadFile(MultipartFile file){ public String uploadFile(MultipartFile file){
Long userId = SessionContext.getSession().getId();
// 大小校验 // 大小校验
if(file.getSize() > Constant.MAX_FILE_SIZE){ if(file.getSize() > Constant.MAX_FILE_SIZE){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"文件大小不能超过10M"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"文件大小不能超过10M");
@ -59,11 +61,14 @@ public class FileService {
if(StringUtils.isEmpty(fileName)){ if(StringUtils.isEmpty(fileName)){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"文件上传失败"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"文件上传失败");
} }
return generUrl(FileTypeEnum.FILE,fileName); String url = generUrl(FileTypeEnum.FILE,fileName);
log.info("文件文件成功,用户id:{},url:{}",userId,url);
return url;
} }
public UploadImageVO uploadImage(MultipartFile file){ public UploadImageVO uploadImage(MultipartFile file){
try { try {
Long userId = SessionContext.getSession().getId();
// 大小校验 // 大小校验
if(file.getSize() > Constant.MAX_IMAGE_SIZE){ if(file.getSize() > Constant.MAX_IMAGE_SIZE){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片大小不能超过5M"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片大小不能超过5M");
@ -86,6 +91,7 @@ public class FileService {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片上传失败"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片上传失败");
} }
vo.setThumbUrl(generUrl(FileTypeEnum.IMAGE,fileName)); vo.setThumbUrl(generUrl(FileTypeEnum.IMAGE,fileName));
log.info("文件图片成功,用户id:{},url:{}",userId,vo.getOriginUrl());
return vo; return vo;
} catch (IOException e) { } catch (IOException e) {
log.error("上传图片失败,{}",e.getMessage(),e); log.error("上传图片失败,{}",e.getMessage(),e);

2
im-platform/src/main/java/com/bx/implatform/util/FileUtil.java

@ -21,7 +21,7 @@ public class FileUtil {
*/ */
public static boolean isImage(String fileName) { public static boolean isImage(String fileName) {
String extension = getFileExtension(fileName); String extension = getFileExtension(fileName);
String[] imageExtension = new String[]{"jpeg", "jpg", "bmp", "png","webp"}; String[] imageExtension = new String[]{"jpeg", "jpg", "bmp", "png","webp","gif"};
for (String e : imageExtension){ for (String e : imageExtension){
if (extension.toLowerCase().equals(e)) { if (extension.toLowerCase().equals(e)) {
return true; return true;

2
im-platform/src/main/resources/application.yml

@ -23,7 +23,7 @@ mybatis-plus:
configuration: configuration:
# 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射 # 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射
map-underscore-to-camel-case: false map-underscore-to-camel-case: false
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# mapper # mapper
mapper-locations: mapper-locations:
# *.xml的具体路径 # *.xml的具体路径

1
im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java

@ -14,6 +14,7 @@ public class MessageProtocolEncoder extends MessageToMessageEncoder<SendInfo> {
protected void encode(ChannelHandlerContext channelHandlerContext, SendInfo sendInfo, List<Object> list) throws Exception { protected void encode(ChannelHandlerContext channelHandlerContext, SendInfo sendInfo, List<Object> list) throws Exception {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
String text = objectMapper.writeValueAsString(sendInfo); String text = objectMapper.writeValueAsString(sendInfo);
TextWebSocketFrame frame = new TextWebSocketFrame(text); TextWebSocketFrame frame = new TextWebSocketFrame(text);
list.add(frame); list.add(frame);
} }

21
im-ui/src/components/chat/ChatBox.vue

@ -40,7 +40,7 @@
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div> </div>
<div title="聊天记录" class="el-icon-chat-dot-round"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
<textarea v-model="sendText" ref="sendBox" class="send-text-area" <textarea v-model="sendText" ref="sendBox" class="send-text-area"
@keydown.enter="sendTextMessage()"></textarea> @keydown.enter="sendTextMessage()"></textarea>
@ -57,6 +57,9 @@
</el-main> </el-main>
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> <emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion>
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice> <chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice>
<chat-history :visible="showHistory"
:chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers"
@close="closeHistoryBox"></chat-history>
</el-container> </el-container>
</template> </template>
@ -66,7 +69,9 @@
import FileUpload from "../common/FileUpload.vue"; import FileUpload from "../common/FileUpload.vue";
import Emotion from "../common/Emotion.vue"; import Emotion from "../common/Emotion.vue";
import ChatVoice from "./ChatVoice.vue"; import ChatVoice from "./ChatVoice.vue";
import ChatHistory from "./ChatHistory.vue";
export default { export default {
name: "chatPrivate", name: "chatPrivate",
components: { components: {
@ -74,7 +79,8 @@
FileUpload, FileUpload,
ChatGroupSide, ChatGroupSide,
Emotion, Emotion,
ChatVoice ChatVoice,
ChatHistory
}, },
props: { props: {
chat: { chat: {
@ -93,7 +99,8 @@
emoBoxPos: { // emoji emoBoxPos: { // emoji
x: 0, x: 0,
y: 0 y: 0
} },
showHistory: false //
} }
}, },
methods: { methods: {
@ -211,6 +218,12 @@
closeVoiceBox() { closeVoiceBox() {
this.showVoice = false; this.showVoice = false;
}, },
showHistoryBox(){
this.showHistory = true;
},
closeHistoryBox(){
this.showHistory = false;
},
handleSendVoice(data) { handleSendVoice(data) {
let msgInfo = { let msgInfo = {
content: JSON.stringify(data), content: JSON.stringify(data),

170
im-ui/src/components/chat/ChatHistory.vue

@ -0,0 +1,170 @@
<template>
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="handleClose">
<div class="chat-history" v-loading="loading"
element-loading-text="拼命加载中">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" >
<ul>
<li v-for="(msgInfo,idx) in messages" :key="idx">
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
:msgInfo="msgInfo" :menu="false">
</message-item>
</li>
</ul>
</el-scrollbar>
</div>
</el-drawer>
</template>
<script>
import MessageItem from './MessageItem.vue';
export default {
name: 'chatHistory',
components: {
MessageItem
},
props: {
visible: {
type: Boolean
},
chat: {
type: Object
},
friend: {
type: Object
},
group: {
type: Object
},
groupMembers: {
type: Array,
}
},
data() {
return {
page: 1,
size: 10,
messages: [],
loadAll: false,
loading: false,
lastScrollTime: new Date()
}
},
methods: {
handleClose() {
this.page = 1;
this.messages = [];
this.loadAll = false;
this.$emit('close');
},
handleScroll() {
let high = this.$refs.scrollbar.$refs.wrap.scrollTop; //
let timeDiff = new Date().getTime() - this.lastScrollTime.getTime();
if ( high < 30 && timeDiff>500) {
this.lastScrollTime = new Date();
this.loadMessages();
}
},
loadMessages() {
if(this.loadAll){
return this.$message.success("已到达顶部");
}
let param = {
page: this.page++,
size: this.size
}
if (this.chat.type == 'GROUP') {
param.groupId = this.group.id;
} else {
param.friendId = this.friend.id;
}
this.loading = true;
this.$http({
url: this.histroyAction,
method: 'get',
params: param
}).then(messages => {
messages.forEach(m => this.messages.unshift(m));
this.loading = false;
if(messages.length <this.size){
this.loadAll = true;
}
this.refreshScrollPos();
}).catch(()=>{
this.loading = false;
})
},
showName(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.aliasName : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
}
},
headImage(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.headImage : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
}
},
refreshScrollPos(){
let scrollWrap = this.$refs.scrollbar.$refs.wrap;
let scrollHeight = scrollWrap.scrollHeight;
let scrollTop = scrollWrap.scrollTop;
this.$nextTick(() => {
let offsetTop = scrollWrap.scrollHeight - scrollHeight;
scrollWrap.scrollTop = scrollTop + offsetTop;
//
if(scrollWrap.scrollHeight == scrollHeight){
this.loadMessages();
}
});
}
},
computed: {
mine() {
return this.$store.state.userStore.userInfo;
},
histroyAction() {
return `/message/${this.chat.type.toLowerCase()}/history`;
}
},
watch: {
visible: {
handler(newValue, oldValue) {
if (newValue) {
this.loadMessages();
this.$nextTick(() => {
document.getElementById('historyScrollbar').addEventListener("mousewheel", this.handleScroll,true);
});
}
}
}
}
}
</script>
<style lang="scss">
.chat-history {
display: flex;
height: 100%;
.chat-history-scrollbar {
flex: 1;
.el-scrollbar__thumb {
background-color: #555555;
}
ul {
padding: 20px;
li {
list-style-type: none;
}
}
}
}
</style>

6
im-ui/src/components/chat/MessageItem.vue

@ -37,7 +37,7 @@
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show=false" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show=false"
@select="handleSelectMenu"></right-menu> @select="handleSelectMenu"></right-menu>
</div> </div>
</template> </template>
@ -70,6 +70,10 @@
msgInfo: { msgInfo: {
type: Object, type: Object,
required: true required: true
},
menu:{
type: Boolean,
default: true
} }
}, },
data() { data() {

Loading…
Cancel
Save