Browse Source

!21 2.0版本上线

Merge pull request !21 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
26fa63eb75
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 1
      .gitignore
  2. 21
      im-client/src/main/java/com/bx/imclient/IMClient.java
  3. 24
      im-client/src/main/java/com/bx/imclient/config/RedisConfig.java
  4. 6
      im-client/src/main/java/com/bx/imclient/listener/MessageListener.java
  5. 19
      im-client/src/main/java/com/bx/imclient/listener/MessageListenerMulticaster.java
  6. 167
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  7. 3
      im-client/src/main/java/com/bx/imclient/task/AbstractPullMessageTask.java
  8. 12
      im-client/src/main/java/com/bx/imclient/task/PullSendResultGroupMessageTask.java
  9. 12
      im-client/src/main/java/com/bx/imclient/task/PullSendResultPrivateMessageTask.java
  10. 3
      im-commom/src/main/java/com/bx/imcommon/contant/IMConstant.java
  11. 9
      im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java
  12. 1
      im-commom/src/main/java/com/bx/imcommon/enums/IMCmdType.java
  13. 12
      im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java
  14. 42
      im-commom/src/main/java/com/bx/imcommon/enums/IMTerminalType.java
  15. 42
      im-commom/src/main/java/com/bx/imcommon/model/GroupMessageInfo.java
  16. 43
      im-commom/src/main/java/com/bx/imcommon/model/IMGroupMessage.java
  17. 2
      im-commom/src/main/java/com/bx/imcommon/model/IMHeartbeatInfo.java
  18. 2
      im-commom/src/main/java/com/bx/imcommon/model/IMLoginInfo.java
  19. 44
      im-commom/src/main/java/com/bx/imcommon/model/IMPrivateMessage.java
  20. 20
      im-commom/src/main/java/com/bx/imcommon/model/IMRecvInfo.java
  21. 28
      im-commom/src/main/java/com/bx/imcommon/model/IMSendResult.java
  22. 18
      im-commom/src/main/java/com/bx/imcommon/model/IMSessionInfo.java
  23. 30
      im-commom/src/main/java/com/bx/imcommon/model/IMUserInfo.java
  24. 42
      im-commom/src/main/java/com/bx/imcommon/model/PrivateMessageInfo.java
  25. 24
      im-commom/src/main/java/com/bx/imcommon/model/SendResult.java
  26. 4
      im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java
  27. 3
      im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java
  28. 4
      im-platform/src/main/java/com/bx/implatform/config/MvcConfig.java
  29. 6
      im-platform/src/main/java/com/bx/implatform/config/RedisConfig.java
  30. 8
      im-platform/src/main/java/com/bx/implatform/contant/Constant.java
  31. 4
      im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
  32. 2
      im-platform/src/main/java/com/bx/implatform/controller/FriendController.java
  33. 4
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  34. 8
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  35. 9
      im-platform/src/main/java/com/bx/implatform/controller/LoginController.java
  36. 8
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  37. 14
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  38. 79
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  39. 28
      im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java
  40. 15
      im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java
  41. 21
      im-platform/src/main/java/com/bx/implatform/dto/ModifyPwdDTO.java
  42. 31
      im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java
  43. 2
      im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java
  44. 5
      im-platform/src/main/java/com/bx/implatform/entity/User.java
  45. 13
      im-platform/src/main/java/com/bx/implatform/enums/FileType.java
  46. 14
      im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java
  47. 14
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  48. 1
      im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java
  49. 2
      im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java
  50. 1
      im-platform/src/main/java/com/bx/implatform/filter/CacheFilter.java
  51. 8
      im-platform/src/main/java/com/bx/implatform/filter/CacheHttpServletRequestWrapper.java
  52. 1
      im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java
  53. 2
      im-platform/src/main/java/com/bx/implatform/interceptor/XssInterceptor.java
  54. 20
      im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java
  55. 41
      im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
  56. 2
      im-platform/src/main/java/com/bx/implatform/service/IFriendService.java
  57. 1
      im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java
  58. 8
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  59. 4
      im-platform/src/main/java/com/bx/implatform/service/IGroupService.java
  60. 8
      im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java
  61. 7
      im-platform/src/main/java/com/bx/implatform/service/IUserService.java
  62. 32
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  63. 14
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  64. 28
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  65. 123
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  66. 88
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  67. 104
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  68. 74
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  69. 245
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  70. 4
      im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java
  71. 13
      im-platform/src/main/java/com/bx/implatform/session/UserSession.java
  72. 32
      im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java
  73. 1081
      im-platform/src/main/java/com/bx/implatform/util/DateTimeUtils.java
  74. 40
      im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java
  75. 3
      im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java
  76. 1
      im-platform/src/main/java/com/bx/implatform/vo/LoginVO.java
  77. 41
      im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java
  78. 6
      im-platform/src/main/java/com/bx/implatform/vo/UserVO.java
  79. 7
      im-platform/src/main/resources/db/db.sql
  80. 1
      im-server/src/main/java/com/bx/imserver/IMServerApp.java
  81. 23
      im-server/src/main/java/com/bx/imserver/config/RedisConfig.java
  82. 12
      im-server/src/main/java/com/bx/imserver/constant/ChannelAttrKey.java
  83. 25
      im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java
  84. 4
      im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java
  85. 30
      im-server/src/main/java/com/bx/imserver/netty/UserChannelCtxMap.java
  86. 2
      im-server/src/main/java/com/bx/imserver/netty/processor/AbstractMessageProcessor.java
  87. 62
      im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java
  88. 33
      im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java
  89. 48
      im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java
  90. 57
      im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java
  91. 12
      im-server/src/main/java/com/bx/imserver/netty/processor/ProcessorFactory.java
  92. 2
      im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java
  93. 2
      im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java
  94. 28
      im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java
  95. 28
      im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java
  96. 25
      im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java
  97. BIN
      im-ui/public/favicon.ico
  98. 2
      im-ui/public/index.html
  99. BIN
      im-ui/public/logo.png
  100. 6
      im-ui/src/api/enums.js

1
.gitignore

@ -7,3 +7,4 @@
/im-server/src/main/resources/application-prod.yml /im-server/src/main/resources/application-prod.yml
/im-server/src/main/resources/logback-prod.xml /im-server/src/main/resources/logback-prod.xml
/im-commom/im-commom.iml /im-commom/im-commom.iml
/im-uniapp/node_modules/

21
im-client/src/main/java/com/bx/imclient/IMClient.java

@ -1,13 +1,12 @@
package com.bx.imclient; package com.bx.imclient;
import com.bx.imclient.listener.MessageListenerMulticaster;
import com.bx.imclient.sender.IMSender; import com.bx.imclient.sender.IMSender;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.IMPrivateMessage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration @Configuration
public class IMClient { public class IMClient {
@ -27,21 +26,19 @@ public class IMClient {
/** /**
* 发送私聊消息发送结果通过MessageListener接收 * 发送私聊消息发送结果通过MessageListener接收
* *
* @param recvId 接收用户id * @param message 私有消息
* @param messageInfo 消息体将转成json发送到客户端
*/ */
public void sendPrivateMessage(Long recvId, PrivateMessageInfo... messageInfo){ public void sendPrivateMessage(IMPrivateMessage<?> message){
imSender.sendPrivateMessage(recvId,messageInfo); imSender.sendPrivateMessage(message);
} }
/** /**
* 发送群聊消息发送结果通过MessageListener接收 * 发送群聊消息发送结果通过MessageListener接收
* *
* @param recvIds 群聊用户id列表 * @param message 群聊消息
* @param messageInfo 消息体将转成json发送到客户端
*/ */
public void sendGroupMessage(List<Long> recvIds, GroupMessageInfo... messageInfo){ public void sendGroupMessage(IMGroupMessage<?> message){
imSender.sendGroupMessage(recvIds,messageInfo); imSender.sendGroupMessage(message);
} }

24
im-client/src/main/java/com/bx/imclient/config/RedisConfig.java

@ -1,5 +1,6 @@
package com.bx.imclient.config; package com.bx.imclient.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
@ -9,6 +10,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
@ -17,16 +19,13 @@ import javax.annotation.Resource;
@Configuration("IMRedisConfig") @Configuration("IMRedisConfig")
public class RedisConfig { public class RedisConfig {
@Resource
private RedisConnectionFactory factory;
@Bean("IMRedisTemplate") @Bean("IMRedisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置值(value)的序列化采用jackson2JsonRedisSerializer // 设置值(value)的序列化采用FastJsonRedisSerializer
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(fastJsonRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer());
// 设置键(key)的序列化采用StringRedisSerializer。 // 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer());
@ -34,16 +33,9 @@ public class RedisConfig {
return redisTemplate; return redisTemplate;
} }
@Bean public FastJsonRedisSerializer fastJsonRedisSerializer(){
public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ FastJsonRedisSerializer <Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); return fastJsonRedisSerializer;
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 解决jackson2无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
} }
} }

6
im-client/src/main/java/com/bx/imclient/listener/MessageListener.java

@ -1,10 +1,10 @@
package com.bx.imclient.listener; package com.bx.imclient.listener;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
public interface MessageListener { public interface MessageListener<T> {
void process(SendResult result); void process(IMSendResult<T> result);
} }

19
im-client/src/main/java/com/bx/imclient/listener/MessageListenerMulticaster.java

@ -1,12 +1,15 @@
package com.bx.imclient.listener; package com.bx.imclient.listener;
import com.alibaba.fastjson.JSONObject;
import com.bx.imclient.annotation.IMListener; import com.bx.imclient.annotation.IMListener;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -16,12 +19,22 @@ public class MessageListenerMulticaster {
@Autowired(required = false) @Autowired(required = false)
private List<MessageListener> messageListeners = Collections.emptyList(); private List<MessageListener> messageListeners = Collections.emptyList();
public void multicast(IMListenerType type, SendResult result){ public void multicast(IMListenerType listenerType, IMSendResult result){
for(MessageListener listener:messageListeners){ for(MessageListener listener:messageListeners){
IMListener annotation = listener.getClass().getAnnotation(IMListener.class); IMListener annotation = listener.getClass().getAnnotation(IMListener.class);
if(annotation!=null && (annotation.type().equals(IMListenerType.ALL) || annotation.type().equals(type))){ if(annotation!=null && (annotation.type().equals(IMListenerType.ALL) || annotation.type().equals(listenerType))){
// 将data转回对象类型
if(result.getData() instanceof JSONObject){
Type superClass = listener.getClass().getGenericInterfaces()[0];
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
JSONObject data = (JSONObject)result.getData();
result.setData(data.toJavaObject(type));
}
// 回调到调用方处理
listener.process(result); listener.process(result);
} }
} }
} }
} }

167
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@ -1,14 +1,12 @@
package com.bx.imclient.sender; package com.bx.imclient.sender;
import com.bx.imclient.listener.MessageListenerMulticaster; import com.bx.imclient.listener.MessageListenerMulticaster;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendCode; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.*;
import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.imcommon.model.SendResult;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@ -25,91 +23,130 @@ public class IMSender {
@Autowired @Autowired
@Qualifier("IMRedisTemplate") @Qualifier("IMRedisTemplate")
private RedisTemplate redisTemplate; private RedisTemplate<String, Object> redisTemplate;
@Autowired @Autowired
private MessageListenerMulticaster listenerMulticaster; private MessageListenerMulticaster listenerMulticaster;
public void sendPrivateMessage(Long recvId, PrivateMessageInfo... messageInfos){ public void sendPrivateMessage(IMPrivateMessage<?> message) {
for (Integer terminal : message.getRecvTerminals()) {
// 获取对方连接的channelId // 获取对方连接的channelId
String key = RedisKey.IM_USER_SERVER_ID + recvId; String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getRecvId().toString(), terminal.toString());
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_QUEUE + serverId; String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
IMRecvInfo[] recvInfos = new IMRecvInfo[messageInfos.length]; IMRecvInfo recvInfo = new IMRecvInfo();
for (int i=0;i<messageInfos.length;i++){
IMRecvInfo<PrivateMessageInfo> recvInfo = new IMRecvInfo<>();
recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code());
List recvIds = new LinkedList(); recvInfo.setSendResult(message.getSendResult());
recvIds.add(recvId); recvInfo.setSender(message.getSender());
recvInfo.setRecvIds(recvIds); recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getRecvId(), terminal)));
recvInfo.setData(messageInfos[i]); recvInfo.setData(message.getData());
recvInfos[i] = recvInfo; redisTemplate.opsForList().rightPush(sendKey, recvInfo);
} } else if (message.getSendResult()) {
redisTemplate.opsForList().rightPushAll(sendKey, recvInfos);
}else{
// 回复消息状态 // 回复消息状态
for(PrivateMessageInfo messageInfo : messageInfos ) { IMSendResult result = new IMSendResult();
SendResult result = new SendResult(); result.setSender(message.getSender());
result.setMessageInfo(messageInfo); result.setReceiver(new IMUserInfo(message.getRecvId(), terminal));
result.setRecvId(recvId); result.setCode(IMSendCode.NOT_ONLINE.code());
result.setCode(IMSendCode.NOT_ONLINE); result.setData(message.getData());
listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result); listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result);
} }
} }
// 推送给自己的其他终端
if(message.getSendToSelf()){
for (Integer terminal : IMTerminalType.codes()) {
if (message.getSender().getTerminal().equals(terminal)) {
continue;
} }
// 获取终端连接的channelId
public void sendGroupMessage(List<Long> recvIds, GroupMessageInfo... messageInfos){ String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getSender().getId().toString(), terminal.toString());
// 根据群聊每个成员所连的IM-server,进行分组
List<Long> offLineIds = Collections.synchronizedList(new LinkedList<Long>());
Map<Integer, List<Long>> serverMap = new ConcurrentHashMap<>();
recvIds.parallelStream().forEach(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){ // 如果终端在线,将数据存储至redis,等待拉取推送
// 此处需要加锁,否则list可以会被覆盖 if (serverId != null) {
synchronized(serverMap){ String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
if(serverMap.containsKey(serverId)){ IMRecvInfo recvInfo = new IMRecvInfo();
serverMap.get(serverId).add(id); // 自己的消息不需要回推消息结果
}else { recvInfo.setSendResult(false);
List<Long> list = Collections.synchronizedList(new LinkedList<Long>()); recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code());
list.add(id); recvInfo.setSender(message.getSender());
serverMap.put(serverId,list); recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getSender().getId(), terminal)));
recvInfo.setData(message.getData());
redisTemplate.opsForList().rightPush(sendKey, recvInfo);
} }
} }
}else{ }
offLineIds.add(id);
}
public void sendGroupMessage(IMGroupMessage<?> message) {
// 根据群聊每个成员所连的IM-server,进行分组
List<IMUserInfo> offLineUsers = Collections.synchronizedList(new LinkedList<>());
// 格式:map<服务器id,list<接收方>>
Map<Integer, List<IMUserInfo>> serverMap = new ConcurrentHashMap<>();
for (Integer terminal : message.getRecvTerminals()) {
message.getRecvIds().parallelStream().forEach(id -> {
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString());
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
if (serverId != null) {
List<IMUserInfo> list = serverMap.computeIfAbsent(serverId, o -> Collections.synchronizedList(new LinkedList<>()));
list.add(new IMUserInfo(id, terminal));
} else {
// 加入离线列表
offLineUsers.add(new IMUserInfo(id, terminal));
} }
}); });
}
// 逐个server发送 // 逐个server发送
for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) { for (Map.Entry<Integer, List<IMUserInfo>> entry : serverMap.entrySet()) {
IMRecvInfo[] recvInfos = new IMRecvInfo[messageInfos.length]; IMRecvInfo recvInfo = new IMRecvInfo();
for (int i=0;i<messageInfos.length;i++){
IMRecvInfo<GroupMessageInfo> recvInfo = new IMRecvInfo<>();
recvInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); recvInfo.setCmd(IMCmdType.GROUP_MESSAGE.code());
recvInfo.setRecvIds(new LinkedList<>(entry.getValue())); recvInfo.setReceivers(new LinkedList<>(entry.getValue()));
recvInfo.setData(messageInfos[i]); recvInfo.setSender(message.getSender());
recvInfos[i] = recvInfo; recvInfo.setSendResult(message.getSendResult());
recvInfo.setData(message.getData());
// 推送至队列
String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, entry.getKey().toString());
redisTemplate.opsForList().rightPush(key, recvInfo);
} }
String key = RedisKey.IM_UNREAD_GROUP_QUEUE +entry.getKey(); // 对离线用户回复消息状态
redisTemplate.opsForList().rightPushAll(key,recvInfos); if (message.getSendResult()) {
for (IMUserInfo offLineUser : offLineUsers) {
IMSendResult result = new IMSendResult();
result.setSender(message.getSender());
result.setReceiver(offLineUser);
result.setCode(IMSendCode.NOT_ONLINE.code());
result.setData(message.getData());
listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE, result);
}
}
// 推送给自己的其他终端
if (message.getSendToSelf()) {
for (Integer terminal : IMTerminalType.codes()) {
if (terminal.equals(message.getSender().getTerminal())) {
continue;
}
// 获取终端连接的channelId
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getSender().getId().toString(), terminal.toString());
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
// 如果终端在线,将数据存储至redis,等待拉取推送
if (serverId != null) {
IMRecvInfo recvInfo = new IMRecvInfo();
recvInfo.setCmd(IMCmdType.GROUP_MESSAGE.code());
recvInfo.setSender(message.getSender());
recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getSender().getId(), terminal)));
// 自己的消息不需要回推消息结果
recvInfo.setSendResult(false);
recvInfo.setData(message.getData());
String sendKey = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, serverId.toString());
redisTemplate.opsForList().rightPush(sendKey, recvInfo);
} }
// 不在线的用户,回复消息状态
for(GroupMessageInfo messageInfo:messageInfos ){
for(Long id : offLineIds){
// 回复消息状态
SendResult result = new SendResult();
result.setMessageInfo(messageInfo);
result.setRecvId(id);
result.setCode(IMSendCode.NOT_ONLINE);
listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result);
} }
} }
} }
public Boolean isOnline(Long userId){ public Boolean isOnline(Long userId) {
String key = RedisKey.IM_USER_SERVER_ID + userId; String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), "*");
return redisTemplate.hasKey(key); return !redisTemplate.keys(key).isEmpty();
} }
} }

3
im-client/src/main/java/com/bx/imclient/task/AbstractPullMessageTask.java

@ -5,8 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService; import java.util.concurrent.*;
import java.util.concurrent.Executors;
@Slf4j @Slf4j
public abstract class AbstractPullMessageTask { public abstract class AbstractPullMessageTask {

12
im-client/src/main/java/com/bx/imclient/task/PullSendResultGroupMessageTask.java

@ -1,9 +1,10 @@
package com.bx.imclient.task; package com.bx.imclient.task;
import com.alibaba.fastjson.JSONObject;
import com.bx.imclient.listener.MessageListenerMulticaster; import com.bx.imclient.listener.MessageListenerMulticaster;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@ -23,9 +24,10 @@ public class PullSendResultGroupMessageTask extends AbstractPullMessageTask{
@Override @Override
public void pullMessage() { public void pullMessage() {
String key = RedisKey.IM_RESULT_GROUP_QUEUE; String key = IMRedisKey.IM_RESULT_GROUP_QUEUE;
SendResult result = (SendResult)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS); JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
if(result != null) { if(jsonObject != null) {
IMSendResult result = jsonObject.toJavaObject(IMSendResult.class);
listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result); listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result);
} }
} }

12
im-client/src/main/java/com/bx/imclient/task/PullSendResultPrivateMessageTask.java

@ -1,9 +1,10 @@
package com.bx.imclient.task; package com.bx.imclient.task;
import com.alibaba.fastjson.JSONObject;
import com.bx.imclient.listener.MessageListenerMulticaster; import com.bx.imclient.listener.MessageListenerMulticaster;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
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.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -28,9 +29,10 @@ public class PullSendResultPrivateMessageTask extends AbstractPullMessageTask{
@Override @Override
public void pullMessage() { public void pullMessage() {
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; String key = IMRedisKey.IM_RESULT_PRIVATE_QUEUE;
SendResult result = (SendResult)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS); JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
if(result != null) { if(jsonObject != null) {
IMSendResult result = jsonObject.toJavaObject(IMSendResult.class);
listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result); listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result);
} }
} }

3
im-commom/src/main/java/com/bx/imcommon/contant/Constant.java → im-commom/src/main/java/com/bx/imcommon/contant/IMConstant.java

@ -1,7 +1,7 @@
package com.bx.imcommon.contant; package com.bx.imcommon.contant;
public class Constant { public class IMConstant {
// 在线状态过期时间 600s // 在线状态过期时间 600s
public static final long ONLINE_TIMEOUT_SECOND = 600; public static final long ONLINE_TIMEOUT_SECOND = 600;
@ -9,5 +9,4 @@ public class Constant {
public static final long ALLOW_RECALL_SECOND = 300; public static final long ALLOW_RECALL_SECOND = 300;
} }

9
im-commom/src/main/java/com/bx/imcommon/contant/RedisKey.java → im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java

@ -1,19 +1,18 @@
package com.bx.imcommon.contant; package com.bx.imcommon.contant;
public class RedisKey { public class IMRedisKey {
// im-server最大id,从0开始递增 // im-server最大id,从0开始递增
public final static String IM_MAX_SERVER_ID = "im:max_server_id"; public final static String IM_MAX_SERVER_ID = "im:max_server_id";
// 用户ID所连接的IM-server的ID // 用户ID所连接的IM-server的ID
public final static String IM_USER_SERVER_ID = "im:user:server_id:"; public final static String IM_USER_SERVER_ID = "im:user:server_id";
// 未读私聊消息队列 // 未读私聊消息队列
public final static String IM_UNREAD_PRIVATE_QUEUE = "im:unread:private:"; public final static String IM_UNREAD_PRIVATE_QUEUE = "im:unread:private";
// 未读群聊消息队列 // 未读群聊消息队列
public final static String IM_UNREAD_GROUP_QUEUE = "im:unread:group:"; public final static String IM_UNREAD_GROUP_QUEUE = "im:unread:group";
// 私聊消息发送结果队列 // 私聊消息发送结果队列
public final static String IM_RESULT_PRIVATE_QUEUE = "im:result:private"; public final static String IM_RESULT_PRIVATE_QUEUE = "im:result:private";
// 群聊消息发送结果队列 // 群聊消息发送结果队列
public final static String IM_RESULT_GROUP_QUEUE = "im:result:group"; public final static String IM_RESULT_GROUP_QUEUE = "im:result:group";
} }

1
im-commom/src/main/java/com/bx/imcommon/enums/IMCmdType.java

@ -11,6 +11,7 @@ public enum IMCmdType {
GROUP_MESSAGE(4,"群发消息"); GROUP_MESSAGE(4,"群发消息");
private Integer code; private Integer code;
private String desc; private String desc;

12
im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java

@ -8,7 +8,7 @@ public enum IMSendCode {
NOT_FIND_CHANNEL(2,"未找到对方的channel"), NOT_FIND_CHANNEL(2,"未找到对方的channel"),
UNKONW_ERROR(9999,"未知异常"); UNKONW_ERROR(9999,"未知异常");
private int code; private Integer code;
private String desc; private String desc;
// 构造方法 // 构造方法
@ -17,6 +17,16 @@ public enum IMSendCode {
this.desc = desc; this.desc = desc;
} }
public static IMSendCode fromCode(Integer code){
for (IMSendCode typeEnum:values()) {
if (typeEnum.code.equals(code)) {
return typeEnum;
}
}
return null;
}
public String description() { public String description() {
return desc; return desc;
} }

42
im-commom/src/main/java/com/bx/imcommon/enums/IMTerminalType.java

@ -0,0 +1,42 @@
package com.bx.imcommon.enums;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum IMTerminalType {
WEB(0,"web"),
APP(1,"app");
private Integer code;
private String desc;
IMTerminalType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public static IMTerminalType fromCode(Integer code){
for (IMTerminalType typeEnum:values()) {
if (typeEnum.code.equals(code)) {
return typeEnum;
}
}
return null;
}
public static List<Integer> codes(){
return Arrays.stream(values()).map(IMTerminalType::code).collect(Collectors.toList());
}
public String description() {
return desc;
}
public Integer code(){
return this.code;
}
}

42
im-commom/src/main/java/com/bx/imcommon/model/GroupMessageInfo.java

@ -1,42 +0,0 @@
package com.bx.imcommon.model;
import com.bx.imcommon.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.util.Date;
@Data
public class GroupMessageInfo {
/*
* 消息id
*/
private Long id;
/*
* 群聊id
*/
private Long groupId;
/*
* 发送者id
*/
private Long sendId;
/*
* 消息内容
*/
private String content;
/*
* 消息内容类型 具体枚举值由应用层定义
*/
private Integer type;
/**
* 发送时间
*/
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
}

43
im-commom/src/main/java/com/bx/imcommon/model/IMGroupMessage.java

@ -0,0 +1,43 @@
package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMTerminalType;
import lombok.Data;
import java.util.List;
@Data
public class IMGroupMessage<T> {
/**
* 发送方
*/
private IMUserInfo sender;
/**
* 接收者id列表(群成员列表)
*/
private List<Long> recvIds;
/**
* 接收者终端类型,默认全部
*/
private List<Integer> recvTerminals = IMTerminalType.codes();
/**
* 是否发送给自己的其他终端,默认true
*/
private Boolean sendToSelf = true;
/**
* 是否需要回推发送结果,默认true
*/
private Boolean sendResult = true;
/**
* 消息内容
*/
private T data;
}

2
im-commom/src/main/java/com/bx/imcommon/model/HeartbeatInfo.java → im-commom/src/main/java/com/bx/imcommon/model/IMHeartbeatInfo.java

@ -3,5 +3,5 @@ package com.bx.imcommon.model;
import lombok.Data; import lombok.Data;
@Data @Data
public class HeartbeatInfo { public class IMHeartbeatInfo {
} }

2
im-commom/src/main/java/com/bx/imcommon/model/LoginInfo.java → im-commom/src/main/java/com/bx/imcommon/model/IMLoginInfo.java

@ -3,7 +3,7 @@ package com.bx.imcommon.model;
import lombok.Data; import lombok.Data;
@Data @Data
public class LoginInfo { public class IMLoginInfo {
private String accessToken; private String accessToken;
} }

44
im-commom/src/main/java/com/bx/imcommon/model/IMPrivateMessage.java

@ -0,0 +1,44 @@
package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMTerminalType;
import lombok.Data;
import java.util.List;
@Data
public class IMPrivateMessage<T> {
/**
* 发送方
*/
private IMUserInfo sender;
/**
* 接收者id
*/
private Long recvId;
/**
* 接收者终端类型,默认全部
*/
private List<Integer> recvTerminals = IMTerminalType.codes();
/**
* 是否发送给自己的其他终端,默认true
*/
private Boolean sendToSelf = true;
/**
* 是否需要回推发送结果,默认true
*/
private Boolean sendResult = true;
/**
* 消息内容
*/
private T data;
}

20
im-commom/src/main/java/com/bx/imcommon/model/IMRecvInfo.java

@ -5,22 +5,32 @@ import lombok.Data;
import java.util.List; import java.util.List;
@Data @Data
public class IMRecvInfo<T> { public class IMRecvInfo {
/* /*
* 命令类型 * 命令类型 IMCmdType
*/ */
private Integer cmd; private Integer cmd;
/* /*
* 接收者id列表 * 发送方
*/ */
private List<Long> recvIds; private IMUserInfo sender;
/*
* 接收方用户列表
*/
List<IMUserInfo> receivers;
/*
* 是否需要回调发送结果
*/
private Boolean sendResult;
/* /*
* 推送消息体 * 推送消息体
*/ */
private T data; private Object data;
} }

28
im-commom/src/main/java/com/bx/imcommon/model/IMSendResult.java

@ -0,0 +1,28 @@
package com.bx.imcommon.model;
import lombok.Data;
@Data
public class IMSendResult<T> {
/**
* 发送方
*/
private IMUserInfo sender;
/**
* 接收方
*/
private IMUserInfo receiver;
/*
* 发送状态 IMCmdType
*/
private Integer code;
/*
* 消息内容
*/
private T data;
}

18
im-commom/src/main/java/com/bx/imcommon/model/IMSessionInfo.java

@ -0,0 +1,18 @@
package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMTerminalType;
import lombok.Data;
@Data
public class IMSessionInfo {
/*
* 用户id
*/
private Long userId;
/*
* 终端类型
*/
private Integer terminal;
}

30
im-commom/src/main/java/com/bx/imcommon/model/IMUserInfo.java

@ -0,0 +1,30 @@
package com.bx.imcommon.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author: 谢绍许
* @date: 2023-09-24 09:23:11
* @version: 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IMUserInfo {
/**
* 用户id
*/
private Long id;
/**
* 用户终端类型 IMTerminalType
*/
private Integer terminal;
}

42
im-commom/src/main/java/com/bx/imcommon/model/PrivateMessageInfo.java

@ -1,42 +0,0 @@
package com.bx.imcommon.model;
import com.bx.imcommon.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.util.Date;
@Data
public class PrivateMessageInfo {
/*
* 消息id
*/
private long id;
/*
* 发送者id
*/
private Long sendId;
/*
* 接收者id
*/
private Long recvId;
/*
* 发送内容
*/
private String content;
/*
* 消息内容类型 具体枚举值由应用层定义
*/
private Integer type;
/**
* 发送时间
*/
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
}

24
im-commom/src/main/java/com/bx/imcommon/model/SendResult.java

@ -1,24 +0,0 @@
package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMSendCode;
import lombok.Data;
@Data
public class SendResult<T> {
/*
* 接收者id
*/
private Long recvId;
/*
* 发送状态
*/
private IMSendCode code;
/*
* 消息体(透传)
*/
private T messageInfo;
}

4
im-platform/src/main/java/com/bx/implatform/ImplatformApp.java → im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java

@ -12,9 +12,9 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy(exposeProxy = true) @EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan(basePackages = {"com.bx.implatform.mapper"}) @MapperScan(basePackages = {"com.bx.implatform.mapper"})
@SpringBootApplication(exclude= {SecurityAutoConfiguration.class })// 禁用secrity @SpringBootApplication(exclude= {SecurityAutoConfiguration.class })// 禁用secrity
public class ImplatformApp { public class IMPlatformApp {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(ImplatformApp.class,args); SpringApplication.run(IMPlatformApp.class,args);
} }
} }

3
im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java

@ -20,10 +20,9 @@ public class MinIoClientConfig {
@Bean @Bean
public MinioClient minioClient(){ public MinioClient minioClient(){
// 注入minio 客户端 // 注入minio 客户端
MinioClient client = MinioClient.builder() return MinioClient.builder()
.endpoint(endpoint) .endpoint(endpoint)
.credentials(accessKey, secretKey) .credentials(accessKey, secretKey)
.build(); .build();
return client;
} }
} }

4
im-platform/src/main/java/com/bx/implatform/config/MvcConfig.java

@ -16,7 +16,7 @@ public class MvcConfig implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(XssInterceptor()) registry.addInterceptor(xssInterceptor())
.addPathPatterns("/**"); .addPathPatterns("/**");
registry.addInterceptor(authInterceptor()) registry.addInterceptor(authInterceptor())
.addPathPatterns("/**") .addPathPatterns("/**")
@ -30,7 +30,7 @@ public class MvcConfig implements WebMvcConfigurer {
} }
@Bean @Bean
public XssInterceptor XssInterceptor() { public XssInterceptor xssInterceptor() {
return new XssInterceptor(); return new XssInterceptor();
} }

6
im-platform/src/main/java/com/bx/implatform/config/RedisConfig.java

@ -38,7 +38,7 @@ public class RedisConfig extends CachingConfigurerSupport {
@Primary @Primary
@Bean @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置值(value)的序列化采用jackson2JsonRedisSerializer // 设置值(value)的序列化采用jackson2JsonRedisSerializer
@ -52,8 +52,8 @@ public class RedisConfig extends CachingConfigurerSupport {
} }
@Bean @Bean
public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper(); ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 解决jackson2无法反序列化LocalDateTime的问题 // 解决jackson2无法反序列化LocalDateTime的问题

8
im-platform/src/main/java/com/bx/implatform/contant/Constant.java

@ -8,13 +8,5 @@ public class Constant {
public static final long MAX_FILE_SIZE = 10*1024*1024; public static final long MAX_FILE_SIZE = 10*1024*1024;
// 群聊最大人数 // 群聊最大人数
public static final long MAX_GROUP_MEMBER = 500; public static final long MAX_GROUP_MEMBER = 500;
// accessToken 过期时间(半小时)
public static final Integer ACCESS_TOKEN_EXPIRE = 30 * 60;
// refreshToken 过期时间(7天)
public static final Integer REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 ;
// accessToken 加密秘钥
// refreshToken 加密秘钥
public static final String ACCESS_TOKEN_SECRET = "MIIBIjANBgkq";
public static final String REFRESH_TOKEN_SECRET = "IKDiqVmn0VFU";
} }

4
im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java

@ -3,7 +3,9 @@ package com.bx.implatform.contant;
public class RedisKey { public class RedisKey {
// 已读群聊消息位置(已读最大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";
// webrtc 会话信息
public final static String IM_WEBRTC_SESSION = "im:webrtc:session";
// 缓存前缀 // 缓存前缀
public final static String IM_CACHE = "im:cache:"; public final static String IM_CACHE = "im:cache:";
// 缓存是否好友:bool // 缓存是否好友:bool

2
im-platform/src/main/java/com/bx/implatform/controller/FriendController.java

@ -27,7 +27,7 @@ public class FriendController {
@GetMapping("/list") @GetMapping("/list")
@ApiOperation(value = "好友列表",notes="获取好友列表") @ApiOperation(value = "好友列表",notes="获取好友列表")
public Result< List<FriendVO>> findFriends(){ public Result< List<FriendVO>> findFriends(){
List<Friend> friends = friendService.findFriendByUserId(SessionContext.getSession().getId()); List<Friend> friends = friendService.findFriendByUserId(SessionContext.getSession().getUserId());
List<FriendVO> vos = friends.stream().map(f->{ List<FriendVO> vos = friends.stream().map(f->{
FriendVO vo = new FriendVO(); FriendVO vo = new FriendVO();
vo.setId(f.getFriendId()); vo.setId(f.getFriendId());

4
im-platform/src/main/java/com/bx/implatform/controller/GroupController.java

@ -28,8 +28,8 @@ public class GroupController {
@ApiOperation(value = "创建群聊",notes="创建群聊") @ApiOperation(value = "创建群聊",notes="创建群聊")
@PostMapping("/create") @PostMapping("/create")
public Result<GroupVO> createGroup(@NotEmpty(message = "群名不能为空") @RequestParam String groupName){ public Result<GroupVO> createGroup(@Valid @RequestBody GroupVO vo){
return ResultUtils.success(groupService.createGroup(groupName)); return ResultUtils.success(groupService.createGroup(vo));
} }
@ApiOperation(value = "修改群聊信息",notes="修改群聊信息") @ApiOperation(value = "修改群聊信息",notes="修改群聊信息")

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

@ -1,11 +1,11 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.implatform.vo.GroupMessageVO;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.IGroupMessageService; import com.bx.implatform.service.IGroupMessageService;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.dto.GroupMessageDTO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -27,7 +27,7 @@ public class GroupMessageController {
@PostMapping("/send") @PostMapping("/send")
@ApiOperation(value = "发送群聊消息",notes="发送群聊消息") @ApiOperation(value = "发送群聊消息",notes="发送群聊消息")
public Result<Long> sendMessage(@Valid @RequestBody GroupMessageVO vo){ public Result<Long> sendMessage(@Valid @RequestBody GroupMessageDTO vo){
return ResultUtils.success(groupMessageService.sendMessage(vo)); return ResultUtils.success(groupMessageService.sendMessage(vo));
} }
@ -47,7 +47,7 @@ public class GroupMessageController {
@GetMapping("/history") @GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录") @ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<GroupMessageInfo>> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId, public Result<List<GroupMessageVO>> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId,
@NotNull(message = "页码不能为空") @RequestParam Long page, @NotNull(message = "页码不能为空") @RequestParam Long page,
@NotNull(message = "size不能为空") @RequestParam Long size){ @NotNull(message = "size不能为空") @RequestParam Long size){
return ResultUtils.success( groupMessageService.findHistoryMessage(groupId,page,size)); return ResultUtils.success( groupMessageService.findHistoryMessage(groupId,page,size));

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

@ -1,6 +1,7 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.IUserService; import com.bx.implatform.service.IUserService;
@ -45,4 +46,12 @@ public class LoginController {
userService.register(dto); userService.register(dto);
return ResultUtils.success(); return ResultUtils.success();
} }
@PutMapping("/modifyPwd")
@ApiOperation(value = "修改密码",notes="修改用户密码")
public Result update(@Valid @RequestBody ModifyPwdDTO dto){
userService.modifyPassword(dto);
return ResultUtils.success();
}
} }

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

@ -1,11 +1,11 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.IPrivateMessageService; import com.bx.implatform.service.IPrivateMessageService;
import com.bx.implatform.vo.PrivateMessageVO; import com.bx.implatform.dto.PrivateMessageDTO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -25,7 +25,7 @@ public class PrivateMessageController {
@PostMapping("/send") @PostMapping("/send")
@ApiOperation(value = "发送消息",notes="发送私聊消息") @ApiOperation(value = "发送消息",notes="发送私聊消息")
public Result<Long> sendMessage(@Valid @RequestBody PrivateMessageVO vo){ public Result<Long> sendMessage(@Valid @RequestBody PrivateMessageDTO vo){
return ResultUtils.success(privateMessageService.sendMessage(vo)); return ResultUtils.success(privateMessageService.sendMessage(vo));
} }
@ -48,7 +48,7 @@ public class PrivateMessageController {
@GetMapping("/history") @GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录") @ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<PrivateMessageInfo>> recallMessage(@NotNull(message = "好友id不能为空") @RequestParam Long friendId, public Result<List<PrivateMessageVO>> recallMessage(@NotNull(message = "好友id不能为空") @RequestParam Long friendId,
@NotNull(message = "页码不能为空") @RequestParam Long page, @NotNull(message = "页码不能为空") @RequestParam Long page,
@NotNull(message = "size不能为空") @RequestParam Long size){ @NotNull(message = "size不能为空") @RequestParam Long size){
return ResultUtils.success( privateMessageService.findHistoryMessage(friendId,page,size)); return ResultUtils.success( privateMessageService.findHistoryMessage(friendId,page,size));

14
im-platform/src/main/java/com/bx/implatform/controller/UserController.java

@ -1,5 +1,6 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.entity.User; import com.bx.implatform.entity.User;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
@ -36,9 +37,9 @@ public class UserController {
@GetMapping("/self") @GetMapping("/self")
@ApiOperation(value = "获取当前用户信息",notes="获取当前用户信息") @ApiOperation(value = "获取当前用户信息",notes="获取当前用户信息")
public Result findSelfInfo(){ public Result<UserVO> findSelfInfo(){
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
User user = userService.getById(session.getId()); User user = userService.getById(session.getUserId());
UserVO userVO = BeanUtils.copyProperties(user,UserVO.class); UserVO userVO = BeanUtils.copyProperties(user,UserVO.class);
return ResultUtils.success(userVO); return ResultUtils.success(userVO);
} }
@ -61,11 +62,16 @@ public class UserController {
@GetMapping("/findByNickName") @GetMapping("/findByNickName")
@ApiOperation(value = "查找用户",notes="根据昵称查找用户") @ApiOperation(value = "查找用户",notes="根据昵称查找用户")
public Result findByNickName(@NotEmpty(message = "用户昵称不可为空") @RequestParam("nickName") String nickName){ public Result<List<UserVO>> findByNickName(@NotEmpty(message = "用户昵称不可为空") @RequestParam("nickName") String nickName){
return ResultUtils.success( userService.findUserByNickName(nickName)); return ResultUtils.success( userService.findUserByNickName(nickName));
} }
@GetMapping("/findByName")
@ApiOperation(value = "查找用户",notes="根据用户名或昵称查找用户")
public Result<List<UserVO>> findByName(@NotEmpty(message = "用户名称不可为空") @RequestParam("name") String name){
return ResultUtils.success( userService.findUserByName(name));
}
} }

79
im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java

@ -1,54 +1,35 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.config.ICEServer;
import com.bx.imclient.IMClient;
import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.implatform.config.ICEServerConfig;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.service.IWebrtcService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
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.*;
import java.util.List;
@Api(tags = "webrtc视频单人通话") @Api(tags = "webrtc视频单人通话")
@RestController @RestController
@RequestMapping("/webrtc/private") @RequestMapping("/webrtc/private")
public class WebrtcController { public class WebrtcController {
@Autowired @Autowired
private IMClient imClient; private IWebrtcService webrtcService;
@Autowired
private ICEServerConfig iceServerConfig;
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话") @ApiOperation(httpMethod = "POST", value = "呼叫视频通话")
@PostMapping("/call") @PostMapping("/call")
public Result call(@RequestParam Long uid, @RequestBody String offer) { public Result call(@RequestParam Long uid, @RequestBody String offer) {
Long userId = SessionContext.getSession().getId(); webrtcService.call(uid,offer);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CALL.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(offer);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(httpMethod = "POST", value = "接受视频通话") @ApiOperation(httpMethod = "POST", value = "接受视频通话")
@PostMapping("/accept") @PostMapping("/accept")
public Result accept(@RequestParam Long uid,@RequestBody String answer) { public Result accept(@RequestParam Long uid,@RequestBody String answer) {
Long userId = SessionContext.getSession().getId(); webrtcService.accept(uid,answer);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_ACCEPT.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(answer);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@ -56,71 +37,43 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "拒绝视频通话") @ApiOperation(httpMethod = "POST", value = "拒绝视频通话")
@PostMapping("/reject") @PostMapping("/reject")
public Result reject(@RequestParam Long uid) { public Result reject(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId(); webrtcService.reject(uid);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_REJECT.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(httpMethod = "POST", value = "取消呼叫") @ApiOperation(httpMethod = "POST", value = "取消呼叫")
@PostMapping("/cancel") @PostMapping("/cancel")
public Result cancel(@RequestParam Long uid) { public Result cancel(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId(); webrtcService.cancel(uid);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CANCEL.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(httpMethod = "POST", value = "呼叫失败") @ApiOperation(httpMethod = "POST", value = "呼叫失败")
@PostMapping("/failed") @PostMapping("/failed")
public Result failed(@RequestParam Long uid,@RequestParam String reason) { public Result failed(@RequestParam Long uid,@RequestParam String reason) {
Long userId = SessionContext.getSession().getId(); webrtcService.failed(uid,reason);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_FAILED.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(reason);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(httpMethod = "POST", value = "挂断") @ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup") @PostMapping("/handup")
public Result leave(@RequestParam Long uid) { public Result leave(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId(); webrtcService.leave(uid);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_HANDUP.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@PostMapping("/candidate") @PostMapping("/candidate")
@ApiOperation(httpMethod = "POST", value = "同步candidate") @ApiOperation(httpMethod = "POST", value = "同步candidate")
public Result candidate(@RequestParam Long uid,@RequestBody String candidate ) { public Result forwardCandidate(@RequestParam Long uid,@RequestBody String candidate ) {
Long userId = SessionContext.getSession().getId(); webrtcService.candidate(uid,candidate);
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CANDIDATE.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(candidate);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping("/iceservers") @GetMapping("/iceservers")
@ApiOperation(httpMethod = "GET", value = "获取iceservers") @ApiOperation(httpMethod = "GET", value = "获取iceservers")
public Result iceservers() { public Result<List<ICEServer>> iceservers() {
return ResultUtils.success(iceServerConfig.getIceServers()); return ResultUtils.success(webrtcService.getIceServers());
} }
} }

28
im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java

@ -0,0 +1,28 @@
package com.bx.implatform.dto;
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;
@Data
@ApiModel("群聊消息DTO")
public class GroupMessageDTO {
@NotNull(message="群聊id不可为空")
@ApiModelProperty(value = "群聊id")
private Long groupId;
@Length(max=1024,message = "内容长度不得大于1024")
@NotEmpty(message="发送内容不可为空")
@ApiModelProperty(value = "发送内容")
private String content;
@NotNull(message="消息类型不可为空")
@ApiModelProperty(value = "消息类型")
private Integer type;
}

15
im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java

@ -5,17 +5,26 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Data @Data
@ApiModel("用户登录VO") @ApiModel("用户登录DTO")
public class LoginDTO { public class LoginDTO {
//@NotEmpty(message="用户名不可为空") @Max(value = 1,message = "登录终端类型取值范围:0,1")
@Min(value = 0,message = "登录终端类型取值范围:0,1")
@NotNull(message="登录终端类型不可为空")
@ApiModelProperty(value = "登录终端 0:web 1:app")
private Integer terminal;
@NotEmpty(message="用户名不可为空")
@ApiModelProperty(value = "用户名") @ApiModelProperty(value = "用户名")
private String userName; private String userName;
// @NotEmpty(message="用户密码不可为空") @NotEmpty(message="用户密码不可为空")
@ApiModelProperty(value = "用户密码") @ApiModelProperty(value = "用户密码")
private String password; private String password;

21
im-platform/src/main/java/com/bx/implatform/dto/ModifyPwdDTO.java

@ -0,0 +1,21 @@
package com.bx.implatform.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@Data
@ApiModel("修改密码DTO")
public class ModifyPwdDTO {
@NotEmpty(message="旧用户密码不可为空")
@ApiModelProperty(value = "旧用户密码")
private String oldPassword;
@NotEmpty(message="新用户密码不可为空")
@ApiModelProperty(value = "新用户密码")
private String newPassword;
}

31
im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java

@ -0,0 +1,31 @@
package com.bx.implatform.dto;
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;
@Data
@ApiModel("私聊消息DTO")
public class PrivateMessageDTO {
@NotNull(message="接收用户id不可为空")
@ApiModelProperty(value = "接收用户id")
private Long recvId;
@Length(max=1024,message = "内容长度不得大于1024")
@NotEmpty(message="发送内容不可为空")
@ApiModelProperty(value = "发送内容")
private String content;
@NotNull(message="消息类型不可为空")
@ApiModelProperty(value = "消息类型")
private Integer type;
}

2
im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java

@ -8,7 +8,7 @@ import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
@Data @Data
@ApiModel("用户注册VO") @ApiModel("用户注册DTO")
public class RegisterDTO { public class RegisterDTO {
@Length(max = 64,message = "用户名不能大于64字符") @Length(max = 64,message = "用户名不能大于64字符")

5
im-platform/src/main/java/com/bx/implatform/entity/User.java

@ -62,6 +62,11 @@ public class User extends Model<User> {
@TableField("head_image_thumb") @TableField("head_image_thumb")
private String headImageThumb; private String headImageThumb;
/**
* 用户类型 1:普通用户 2:审核专用账户
*/
@TableField("type")
private Integer type;
/** /**
* 个性签名 * 个性签名

13
im-platform/src/main/java/com/bx/implatform/enums/FileType.java

@ -9,24 +9,15 @@ public enum FileType {
private Integer code; private final Integer code;
private String desc; private final String desc;
FileType(Integer index, String desc) { FileType(Integer index, String desc) {
this.code =index; this.code =index;
this.desc=desc; this.desc=desc;
} }
public static FileType fromCode(Integer code){
for (FileType typeEnum:values()) {
if (typeEnum.code.equals(code)) {
return typeEnum;
}
}
return null;
}
public String description() { public String description() {
return desc; return desc;

14
im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java

@ -7,25 +7,15 @@ public enum MessageStatus {
ALREADY_READ(1,"已读"), ALREADY_READ(1,"已读"),
RECALL(2,"已撤回"); RECALL(2,"已撤回");
private Integer code; private final Integer code;
private String desc; private final String desc;
MessageStatus(Integer index, String desc) { MessageStatus(Integer index, String desc) {
this.code =index; this.code =index;
this.desc=desc; this.desc=desc;
} }
public static MessageStatus fromCode(Integer code){
for (MessageStatus typeEnum:values()) {
if (typeEnum.code.equals(code)) {
return typeEnum;
}
}
return null;
}
public String description() { public String description() {
return desc; return desc;
} }

14
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -4,10 +4,11 @@ package com.bx.implatform.enums;
public enum MessageType { public enum MessageType {
TEXT(0,"文字"), TEXT(0,"文字"),
FILE(1,"文件"), IMAGE(1,"图片"),
IMAGE(2,"图片"), FILE(2,"文件"),
VIDEO(3,"视频"), AUDIO(3,"音频"),
TIP(10,"系统提示"), VIDEO(4,"视频"),
RECALL(10,"撤回"),
RTC_CALL(101,"呼叫"), RTC_CALL(101,"呼叫"),
RTC_ACCEPT(102,"接受"), RTC_ACCEPT(102,"接受"),
@ -17,16 +18,15 @@ public enum MessageType {
RTC_HANDUP(106,"挂断"), RTC_HANDUP(106,"挂断"),
RTC_CANDIDATE(107,"同步candidate"); RTC_CANDIDATE(107,"同步candidate");
private Integer code; private final Integer code;
private String desc; private final String desc;
MessageType(Integer index, String desc) { MessageType(Integer index, String desc) {
this.code =index; this.code =index;
this.desc=desc; this.desc=desc;
} }
public String description() { public String description() {
return desc; return desc;
} }

1
im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java

@ -21,7 +21,6 @@ public enum ResultCode {
private int code; private int code;
private String msg; private String msg;
// 构造方法
ResultCode(int code, String msg) { ResultCode(int code, String msg) {
this.code = code; this.code = code;
this.msg = msg; this.msg = msg;

2
im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java

@ -71,7 +71,7 @@ public class GlobalExceptionHandler {
public Result handleValidationExceptionHandler(MethodArgumentNotValidException exception) { public Result handleValidationExceptionHandler(MethodArgumentNotValidException exception) {
BindingResult bindResult = exception.getBindingResult(); BindingResult bindResult = exception.getBindingResult();
String msg; String msg;
if (bindResult != null && bindResult.hasErrors()) { if (bindResult.hasErrors()) {
msg = bindResult.getAllErrors().get(0).getDefaultMessage(); msg = bindResult.getAllErrors().get(0).getDefaultMessage();
if (msg.contains("NumberFormatException")) { if (msg.contains("NumberFormatException")) {
msg = "参数类型错误!"; msg = "参数类型错误!";

1
im-platform/src/main/java/com/bx/implatform/filter/CacheFilter.java

@ -7,7 +7,6 @@ import javax.servlet.*;
import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.util.List;
@Component @Component
@ServletComponentScan @ServletComponentScan

8
im-platform/src/main/java/com/bx/implatform/filter/CacheHttpServletRequestWrapper.java

@ -10,9 +10,9 @@ import java.io.*;
public class CacheHttpServletRequestWrapper extends HttpServletRequestWrapper { public class CacheHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody; private byte[] requestBody;
private HttpServletRequest request; private final HttpServletRequest request;
public CacheHttpServletRequestWrapper(HttpServletRequest request) throws IOException { public CacheHttpServletRequestWrapper(HttpServletRequest request) {
super(request); super(request);
this.request = request; this.request = request;
} }
@ -48,10 +48,6 @@ public class CacheHttpServletRequestWrapper extends HttpServletRequestWrapper {
}; };
} }
public byte[] getRequestBody() {
return requestBody;
}
@Override @Override
public BufferedReader getReader() throws IOException { public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream())); return new BufferedReader(new InputStreamReader(this.getInputStream()));

1
im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java

@ -14,7 +14,6 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
;
@Slf4j @Slf4j
public class AuthInterceptor implements HandlerInterceptor { public class AuthInterceptor implements HandlerInterceptor {

2
im-platform/src/main/java/com/bx/implatform/interceptor/XssInterceptor.java

@ -17,7 +17,7 @@ public class XssInterceptor implements HandlerInterceptor {
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 检查参数 // 检查参数
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
for(String[] values:paramMap.values()){ for(String[] values:paramMap.values()){

20
im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java

@ -4,10 +4,9 @@ import com.bx.imclient.annotation.IMListener;
import com.bx.imclient.listener.MessageListener; import com.bx.imclient.listener.MessageListener;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendCode; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.implatform.vo.GroupMessageVO;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.enums.MessageType;
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;
@ -15,22 +14,17 @@ import org.springframework.data.redis.core.RedisTemplate;
@Slf4j @Slf4j
@IMListener(type = IMListenerType.GROUP_MESSAGE) @IMListener(type = IMListenerType.GROUP_MESSAGE)
public class GroupMessageListener implements MessageListener { public class GroupMessageListener implements MessageListener<GroupMessageVO> {
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@Override @Override
public void process(SendResult result){ public void process(IMSendResult<GroupMessageVO> result){
GroupMessageInfo messageInfo = (GroupMessageInfo) result.getMessageInfo(); GroupMessageVO messageInfo = result.getData();
// 提示类数据不记录
if(messageInfo.getType().equals(MessageType.TIP)){
return;
}
// 保存该用户已拉取的最大消息id // 保存该用户已拉取的最大消息id
if(result.getCode().equals(IMSendCode.SUCCESS)) { if(result.getCode().equals(IMSendCode.SUCCESS.code())) {
String key = RedisKey.IM_GROUP_READED_POSITION + messageInfo.getGroupId() + ":" + result.getRecvId(); String key = String.join(":",RedisKey.IM_GROUP_READED_POSITION,messageInfo.getGroupId().toString(),result.getReceiver().getId().toString());
redisTemplate.opsForValue().set(key, messageInfo.getId()); redisTemplate.opsForValue().set(key, messageInfo.getId());
} }
} }

41
im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java

@ -1,63 +1,38 @@
package com.bx.implatform.listener; package com.bx.implatform.listener;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.bx.imclient.IMClient;
import com.bx.imclient.annotation.IMListener; import com.bx.imclient.annotation.IMListener;
import com.bx.imclient.listener.MessageListener; import com.bx.imclient.listener.MessageListener;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendCode; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.service.IPrivateMessageService; import com.bx.implatform.service.IPrivateMessageService;
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 java.util.Date;
@Slf4j @Slf4j
@IMListener(type = IMListenerType.PRIVATE_MESSAGE) @IMListener(type = IMListenerType.PRIVATE_MESSAGE)
public class PrivateMessageListener implements MessageListener { public class PrivateMessageListener implements MessageListener<PrivateMessageVO> {
@Autowired @Autowired
private IPrivateMessageService privateMessageService; private IPrivateMessageService privateMessageService;
@Autowired
private IMClient imClient;
@Override @Override
public void process(SendResult result){ public void process(IMSendResult<PrivateMessageVO> result){
PrivateMessageInfo messageInfo = (PrivateMessageInfo) result.getMessageInfo(); PrivateMessageVO messageInfo = result.getData();
// 提示类数据不记录 // 更新消息状态,这里只处理成功消息,失败的消息继续保持未读状态
if(messageInfo.getType().equals(MessageType.TIP.code())){ if(result.getCode().equals(IMSendCode.SUCCESS.code())){
return;
}
// 视频通话信令不记录
if(messageInfo.getType() >= MessageType.RTC_CALL.code() && messageInfo.getType()< MessageType.RTC_CANDIDATE.code()){
// 通知用户呼叫失败了
if(messageInfo.getType().equals(MessageType.RTC_CALL.code())
&& !result.getCode().equals(IMSendCode.SUCCESS)){
PrivateMessageInfo sendMessage = new PrivateMessageInfo();
sendMessage.setRecvId(messageInfo.getSendId());
sendMessage.setSendId(messageInfo.getRecvId());
sendMessage.setType(MessageType.RTC_FAILED.code());
sendMessage.setContent(result.getCode().description());
sendMessage.setSendTime(new Date());
imClient.sendPrivateMessage(sendMessage.getRecvId(),sendMessage);
}
return;
}
// 更新消息状态
if(result.getCode().equals(IMSendCode.SUCCESS)){
UpdateWrapper<PrivateMessage> updateWrapper = new UpdateWrapper<>(); UpdateWrapper<PrivateMessage> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId()) updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId())
.eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code()) .eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code())
.set(PrivateMessage::getStatus, MessageStatus.ALREADY_READ.code()); .set(PrivateMessage::getStatus, MessageStatus.ALREADY_READ.code());
privateMessageService.update(updateWrapper); privateMessageService.update(updateWrapper);
log.info("消息已读,消息id:{},发送者:{},接收者:{}",messageInfo.getId(),messageInfo.getSendId(),messageInfo.getRecvId()); log.info("消息已读,消息id:{},发送者:{},接收者:{},终端:{}",messageInfo.getId(),result.getSender().getId(),result.getReceiver().getId(),result.getReceiver().getTerminal());
} }
} }

2
im-platform/src/main/java/com/bx/implatform/service/IFriendService.java

@ -11,7 +11,7 @@ public interface IFriendService extends IService<Friend> {
Boolean isFriend(Long userId1, Long userId2); Boolean isFriend(Long userId1, Long userId2);
List<Friend> findFriendByUserId(Long UserId); List<Friend> findFriendByUserId(Long userId);
void addFriend(Long friendId); void addFriend(Long friendId);

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

@ -18,6 +18,7 @@ public interface IGroupMemberService extends IService<GroupMember> {
List<Long> findUserIdsByGroupId(Long groupId); List<Long> findUserIdsByGroupId(Long groupId);
boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members); boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members);
void removeByGroupId(Long groupId); void removeByGroupId(Long groupId);

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

@ -1,9 +1,9 @@
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.imcommon.model.GroupMessageInfo;
import com.bx.implatform.entity.GroupMessage;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupMessageVO;
import com.bx.implatform.entity.GroupMessage;
import com.bx.implatform.dto.GroupMessageDTO;
import java.util.List; import java.util.List;
@ -11,11 +11,11 @@ import java.util.List;
public interface IGroupMessageService extends IService<GroupMessage> { public interface IGroupMessageService extends IService<GroupMessage> {
Long sendMessage(GroupMessageVO vo); Long sendMessage(GroupMessageDTO vo);
void recallMessage(Long id); void recallMessage(Long id);
void pullUnreadMessage(); void pullUnreadMessage();
List<GroupMessageInfo> findHistoryMessage(Long groupId, Long page, Long size); List<GroupMessageVO> findHistoryMessage(Long groupId, Long page, Long size);
} }

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

@ -12,7 +12,7 @@ import java.util.List;
public interface IGroupService extends IService<Group> { public interface IGroupService extends IService<Group> {
GroupVO createGroup(String groupName); GroupVO createGroup(GroupVO vo);
GroupVO modifyGroup(GroupVO vo); GroupVO modifyGroup(GroupVO vo);
@ -26,7 +26,7 @@ public interface IGroupService extends IService<Group> {
void invite(GroupInviteVO vo); void invite(GroupInviteVO vo);
Group GetById(Long groupId); Group getById(Long groupId);
GroupVO findById(Long groupId); GroupVO findById(Long groupId);

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

@ -1,20 +1,20 @@
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.imcommon.model.PrivateMessageInfo;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.vo.PrivateMessageVO; import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.dto.PrivateMessageDTO;
import java.util.List; import java.util.List;
public interface IPrivateMessageService extends IService<PrivateMessage> { public interface IPrivateMessageService extends IService<PrivateMessage> {
Long sendMessage(PrivateMessageVO vo); Long sendMessage(PrivateMessageDTO vo);
void recallMessage(Long id); void recallMessage(Long id);
List<PrivateMessageInfo> findHistoryMessage(Long friendId, Long page,Long size); List<PrivateMessageVO> findHistoryMessage(Long friendId, Long page,Long size);
void pullUnreadMessage(); void pullUnreadMessage();

7
im-platform/src/main/java/com/bx/implatform/service/IUserService.java

@ -1,6 +1,7 @@
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.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.entity.User; import com.bx.implatform.entity.User;
import com.bx.implatform.dto.LoginDTO; import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.RegisterDTO; import com.bx.implatform.dto.RegisterDTO;
@ -14,16 +15,20 @@ public interface IUserService extends IService<User> {
LoginVO login(LoginDTO dto); LoginVO login(LoginDTO dto);
void modifyPassword(ModifyPwdDTO dto);
LoginVO refreshToken(String refreshToken); LoginVO refreshToken(String refreshToken);
void register(RegisterDTO dto); void register(RegisterDTO dto);
User findUserByName(String username); User findUserByUserName(String username);
void update(UserVO vo); void update(UserVO vo);
List<UserVO> findUserByNickName(String nickname); List<UserVO> findUserByNickName(String nickname);
List<UserVO> findUserByName(String name);
List<Long> checkOnline(String userIds); List<Long> checkOnline(String userIds);
} }

32
im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java

@ -0,0 +1,32 @@
package com.bx.implatform.service;
import com.bx.implatform.config.ICEServer;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* webrtc 通信服务
* @author
*/
public interface IWebrtcService {
void call(Long uid, String offer);
void accept( Long uid,@RequestBody String answer);
void reject( Long uid);
void cancel( Long uid);
void failed( Long uid, String reason);
void leave( Long uid) ;
void candidate( Long uid, String candidate);
List<ICEServer> getIceServers();
}

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

@ -36,13 +36,13 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
/** /**
* 查询用户的所有好友 * 查询用户的所有好友
* *
* @param UserId 用户id * @param userId 用户id
* @return * @return
*/ */
@Override @Override
public List<Friend> findFriendByUserId(Long UserId) { public List<Friend> findFriendByUserId(Long userId) {
QueryWrapper<Friend> queryWrapper = new QueryWrapper(); QueryWrapper<Friend> queryWrapper = new QueryWrapper();
queryWrapper.lambda().eq(Friend::getUserId,UserId); queryWrapper.lambda().eq(Friend::getUserId,userId);
List<Friend> friends = this.list(queryWrapper); List<Friend> friends = this.list(queryWrapper);
return friends; return friends;
} }
@ -57,7 +57,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
@Transactional @Transactional
@Override @Override
public void addFriend(Long friendId) { public void addFriend(Long friendId) {
long userId = SessionContext.getSession().getId(); long userId = SessionContext.getSession().getUserId();
if(userId == friendId){ if(userId == friendId){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许添加自己为好友"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许添加自己为好友");
} }
@ -78,7 +78,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
@Transactional @Transactional
@Override @Override
public void delFriend(Long friendId) { public void delFriend(Long friendId) {
long userId = SessionContext.getSession().getId(); long userId = SessionContext.getSession().getUserId();
// 互相解除好友关系 // 互相解除好友关系
FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy(); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.unbindFriend(userId,friendId); proxy.unbindFriend(userId,friendId);
@ -113,7 +113,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
*/ */
@Override @Override
public void update(FriendVO vo) { public void update(FriendVO vo) {
long userId = SessionContext.getSession().getId(); long userId = SessionContext.getSession().getUserId();
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); QueryWrapper<Friend> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda() queryWrapper.lambda()
.eq(Friend::getUserId,userId) .eq(Friend::getUserId,userId)
@ -186,7 +186,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
QueryWrapper<Friend> wrapper = new QueryWrapper<>(); QueryWrapper<Friend> wrapper = new QueryWrapper<>();
wrapper.lambda() wrapper.lambda()
.eq(Friend::getUserId,session.getId()) .eq(Friend::getUserId,session.getUserId())
.eq(Friend::getFriendId,friendId); .eq(Friend::getFriendId,friendId);
Friend friend = this.getOne(wrapper); Friend friend = this.getOne(wrapper);
if(friend == null){ if(friend == null){

28
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java

@ -1,7 +1,9 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.entity.GroupMember;
@ -71,8 +73,8 @@ 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(); LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.lambda().eq(GroupMember::getUserId, userId) memberWrapper.eq(GroupMember::getUserId, userId)
.eq(GroupMember::getQuit,false); .eq(GroupMember::getQuit,false);
return this.list(memberWrapper); return this.list(memberWrapper);
} }
@ -86,8 +88,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
*/ */
@Override @Override
public List<GroupMember> findByGroupId(Long groupId) { public List<GroupMember> findByGroupId(Long groupId) {
QueryWrapper<GroupMember> memberWrapper = new QueryWrapper(); LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.lambda().eq(GroupMember::getGroupId, groupId); memberWrapper.eq(GroupMember::getGroupId, groupId);
return this.list(memberWrapper); return this.list(memberWrapper);
} }
@ -101,16 +103,16 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
@Cacheable(key="#groupId") @Cacheable(key="#groupId")
@Override @Override
public List<Long> findUserIdsByGroupId(Long groupId) { public List<Long> findUserIdsByGroupId(Long groupId) {
QueryWrapper<GroupMember> memberWrapper = new QueryWrapper(); LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.lambda().eq(GroupMember::getGroupId, groupId) memberWrapper.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getQuit,false); .eq(GroupMember::getQuit,false);
List<GroupMember> members = this.list(memberWrapper); List<GroupMember> members = this.list(memberWrapper);
return members.stream().map(m->m.getUserId()).collect(Collectors.toList()); return members.stream().map(GroupMember::getUserId).collect(Collectors.toList());
} }
/** /**
*根据群聊id删除移除成员 * 根据群聊id删除移除成员
* *
* @param groupId 群聊id * @param groupId 群聊id
* @return * @return
@ -118,8 +120,8 @@ 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) {
UpdateWrapper<GroupMember> wrapper = new UpdateWrapper(); LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.lambda().eq(GroupMember::getGroupId,groupId) wrapper.eq(GroupMember::getGroupId,groupId)
.set(GroupMember::getQuit,true); .set(GroupMember::getQuit,true);
this.update(wrapper); this.update(wrapper);
} }
@ -134,8 +136,8 @@ 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) {
UpdateWrapper<GroupMember> wrapper = new UpdateWrapper<>(); LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.lambda().eq(GroupMember::getGroupId,groupId) wrapper.eq(GroupMember::getGroupId,groupId)
.eq(GroupMember::getUserId,userId) .eq(GroupMember::getUserId,userId)
.set(GroupMember::getQuit,true); .set(GroupMember::getQuit,true);
this.update(wrapper); this.update(wrapper);

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

@ -1,10 +1,14 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.Constant; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.implatform.vo.GroupMessageVO;
import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.Group; import com.bx.implatform.entity.Group;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.entity.GroupMember;
@ -18,8 +22,9 @@ import com.bx.implatform.service.IGroupMemberService;
import com.bx.implatform.service.IGroupMessageService; import com.bx.implatform.service.IGroupMessageService;
import com.bx.implatform.service.IGroupService; import com.bx.implatform.service.IGroupService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.dto.GroupMessageDTO;
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;
@ -27,37 +32,31 @@ import org.springframework.stereotype.Service;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage> implements IGroupMessageService { public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage> implements IGroupMessageService {
@Autowired @Autowired
private IGroupService groupService; private IGroupService groupService;
@Autowired @Autowired
private IGroupMemberService groupMemberService; private IGroupMemberService groupMemberService;
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@Autowired @Autowired
private IMClient imClient; private IMClient imClient;
/** /**
* 发送群聊消息(与mysql所有交换都要进行缓存) * 发送群聊消息(与mysql所有交换都要进行缓存)
* *
* @param vo * @param dto 群聊消息
* @return 群聊id * @return 群聊id
*/ */
@Override @Override
public Long sendMessage(GroupMessageVO vo) { public Long sendMessage(GroupMessageDTO dto) {
Long userId = SessionContext.getSession().getId(); UserSession session = SessionContext.getSession();
Group group = groupService.getById(vo.getGroupId()); Group group = groupService.getById(dto.getGroupId());
if(group == null){ if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在");
} }
@ -66,20 +65,24 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
// 判断是否在群里 // 判断是否在群里
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if(!userIds.contains(userId)){ if(!userIds.contains(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法发送消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法发送消息");
} }
// 保存消息 // 保存消息
GroupMessage msg = BeanUtils.copyProperties(vo, GroupMessage.class); GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class);
msg.setSendId(userId); msg.setSendId(session.getUserId());
msg.setSendTime(new Date()); msg.setSendTime(new Date());
this.save(msg); this.save(msg);
// 不用发给自己 // 不用发给自己
userIds = userIds.stream().filter(id->!userId.equals(id)).collect(Collectors.toList()); userIds = userIds.stream().filter(id->!session.getUserId().equals(id)).collect(Collectors.toList());
// 群发 // 群发
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
imClient.sendGroupMessage(userIds,msgInfo); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}",userId,vo.getGroupId(),vo.getContent()); sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage);
log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}",session.getUserId(),dto.getGroupId(),dto.getContent());
return msg.getId(); return msg.getId();
} }
@ -93,19 +96,19 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
*/ */
@Override @Override
public void recallMessage(Long id) { public void recallMessage(Long id) {
Long userId = SessionContext.getSession().getId(); UserSession session = SessionContext.getSession();
GroupMessage msg = this.getById(id); GroupMessage 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().equals(userId)){ if(!msg.getSendId().equals(session.getUserId())){
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() > IMConstant.ALLOW_RECALL_SECOND * 1000){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回");
} }
// 判断是否在群里 // 判断是否在群里
GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(),userId); GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(),session.getUserId());
if(member == null){ if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法撤回消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法撤回消息");
} }
@ -115,54 +118,69 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 群发 // 群发
List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
// 不用发给自己 // 不用发给自己
userIds = userIds.stream().filter(uid->userId.equals(uid)).collect(Collectors.toList()); userIds = userIds.stream().filter(uid->!session.getUserId().equals(uid)).collect(Collectors.toList());
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setType(MessageType.TIP.code()); msgInfo.setType(MessageType.RECALL.code());
String content = String.format("'%s'撤回了一条消息",member.getAliasName()); String content = String.format("'%s'撤回了一条消息",member.getAliasName());
msgInfo.setContent(content); msgInfo.setContent(content);
msgInfo.setSendTime(new Date()); msgInfo.setSendTime(new Date());
imClient.sendGroupMessage(userIds,msgInfo);
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}",userId,msg.getGroupId(),msg.getContent()); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
imClient.sendGroupMessage(sendMessage);
// 推给自己其他终端
msgInfo.setContent("你撤回了一条消息");
sendMessage.setSendToSelf(true);
sendMessage.setRecvIds(Collections.emptyList());
sendMessage.setRecvTerminals(Collections.emptyList());
imClient.sendGroupMessage(sendMessage);
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}",session.getUserId(),msg.getGroupId(),msg.getContent());
} }
/** /**
* 异步拉取群聊消息通过websocket异步推送 * 异步拉取群聊消息通过websocket异步推送
* *
* @return
*/ */
@Override @Override
public void pullUnreadMessage() { public void pullUnreadMessage() {
Long userId = SessionContext.getSession().getId(); UserSession session = SessionContext.getSession();
List<Long> recvIds = new LinkedList(); List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
recvIds.add(userId);
List<GroupMember> members = groupMemberService.findByUserId(userId);
for(GroupMember member:members){ for(GroupMember member:members){
// 获取群聊已读的最大消息id,只推送未读消息 // 获取群聊已读的最大消息id,只推送未读消息
String key = RedisKey.IM_GROUP_READED_POSITION + member.getGroupId()+":"+userId; String key = String.join(":",RedisKey.IM_GROUP_READED_POSITION,member.getGroupId().toString(),session.getUserId().toString());
Integer maxReadedId = (Integer)redisTemplate.opsForValue().get(key); Integer maxReadedId = (Integer)redisTemplate.opsForValue().get(key);
QueryWrapper<GroupMessage> wrapper = new QueryWrapper(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId()) wrapper.eq(GroupMessage::getGroupId,member.getGroupId())
.gt(GroupMessage::getSendTime,member.getCreatedTime()) .gt(GroupMessage::getSendTime,member.getCreatedTime())
.ne(GroupMessage::getSendId, userId) .ne(GroupMessage::getSendId, session.getUserId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()); .ne(GroupMessage::getStatus, MessageStatus.RECALL.code());
if(maxReadedId!=null){ if(maxReadedId!=null){
wrapper.lambda().gt(GroupMessage::getId,maxReadedId); wrapper.gt(GroupMessage::getId,maxReadedId);
} }
wrapper.last("limit 100"); wrapper.last("limit 100");
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
if(messages.isEmpty()){ if(messages.isEmpty()){
continue; continue;
} }
// 组装消息,准备推送 // 推送
List<GroupMessageInfo> messageInfos = messages.stream().map(m->{ for (GroupMessage message:messages ){
GroupMessageInfo msgInfo = BeanUtils.copyProperties(m, GroupMessageInfo.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class);
return msgInfo; IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
}).collect(Collectors.toList()); sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
// 只推给自己当前终端
sendMessage.setRecvIds(Collections.singletonList(session.getUserId()));
sendMessage.setRecvTerminals(Collections.singletonList(session.getTerminal()));
sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage);
}
// 发送消息 // 发送消息
GroupMessageInfo[] infoArr = messageInfos.toArray(new GroupMessageInfo[messageInfos.size()]); log.info("拉取未读群聊消息,用户id:{},群聊id:{},数量:{}",session.getUserId(),member.getGroupId(),messages.size());
imClient.sendGroupMessage(Collections.singletonList(userId), infoArr);
log.info("拉取未读群聊消息,用户id:{},群聊id:{},数量:{}",userId,member.getGroupId(),messageInfos.size());
} }
} }
@ -176,11 +194,11 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
* @return 聊天记录列表 * @return 聊天记录列表
*/ */
@Override @Override
public List<GroupMessageInfo> findHistoryMessage(Long groupId, Long page, Long size) { public List<GroupMessageVO> findHistoryMessage(Long groupId, Long page, Long size) {
page = page > 0 ? page:1; page = page > 0 ? page:1;
size = size > 0 ? size:10; size = size > 0 ? size:10;
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getUserId();
Long stIdx = (page-1)* size; long stIdx = (page-1)* size;
// 群聊成员信息 // 群聊成员信息
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,userId); GroupMember member = groupMemberService.findByGroupAndUserId(groupId,userId);
if(member == null || member.getQuit()){ if(member == null || member.getQuit()){
@ -195,10 +213,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
.last("limit "+stIdx + ","+size); .last("limit "+stIdx + ","+size);
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
List<GroupMessageInfo> messageInfos = messages.stream().map(m->{ List<GroupMessageVO> messageInfos = messages.stream().map(m->BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList());
GroupMessageInfo info = BeanUtils.copyProperties(m, GroupMessageInfo.class);
return info;
}).collect(Collectors.toList());
log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}",userId,groupId,messageInfos.size()); log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}",userId,groupId,messageInfos.size());
return messageInfos; return messageInfos;
} }

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

@ -1,6 +1,7 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
@ -30,8 +31,8 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -51,46 +52,41 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Autowired @Autowired
private IFriendService friendsService; private IFriendService friendsService;
/** /**
* 创建新群聊 * 创建新群聊
* *
* @Param groupName 群聊名称 * @param vo 群聊信息
* @return * @return 群聊信息
**/ **/
@Transactional
@Override @Override
public GroupVO createGroup(String groupName) { public GroupVO createGroup(GroupVO vo) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
User user = userService.getById(session.getId()); User user = userService.getById(session.getUserId());
// 保存群组数据 // 保存群组数据
Group group = new Group(); Group group = BeanUtils.copyProperties(vo,Group.class);
group.setName(groupName);
group.setOwnerId(user.getId()); group.setOwnerId(user.getId());
group.setHeadImage(user.getHeadImage());
group.setHeadImageThumb(user.getHeadImageThumb());
this.save(group); this.save(group);
// 把群主加入群 // 把群主加入群
GroupMember groupMember = new GroupMember(); GroupMember groupMember = new GroupMember();
groupMember.setGroupId(group.getId()); groupMember.setGroupId(group.getId());
groupMember.setUserId(user.getId()); groupMember.setUserId(user.getId());
groupMember.setAliasName(user.getNickName()); groupMember.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName());
groupMember.setRemark(groupName); groupMember.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark());
groupMember.setHeadImage(user.getHeadImageThumb()); groupMember.setHeadImage(user.getHeadImageThumb());
groupMemberService.save(groupMember); groupMemberService.save(groupMember);
GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class);
vo.setAliasName(user.getNickName()); vo.setId(group.getId());
vo.setRemark(groupName); vo.setAliasName(groupMember.getAliasName());
vo.setRemark(groupMember.getRemark());
log.info("创建群聊,群聊id:{},群聊名称:{}",group.getId(),group.getName()); log.info("创建群聊,群聊id:{},群聊名称:{}",group.getId(),group.getName());
return vo; return vo;
} }
/** /**
* 修改群聊信息 * 修改群聊信息
* *
* @Param GroupVO 群聊信息 * @param vo 群聊信息
* @return * @return 群聊信息
**/ **/
@CacheEvict(value = "#vo.getId()") @CacheEvict(value = "#vo.getId()")
@Transactional @Transactional
@ -100,12 +96,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 校验是不是群主,只有群主能改信息 // 校验是不是群主,只有群主能改信息
Group group = this.getById(vo.getId()); Group group = this.getById(vo.getId());
// 群主有权修改群基本信息 // 群主有权修改群基本信息
if(group.getOwnerId().equals(session.getId())){ if(group.getOwnerId().equals(session.getUserId()) ){
group = BeanUtils.copyProperties(vo,Group.class); group = BeanUtils.copyProperties(vo,Group.class);
this.updateById(group); this.updateById(group);
} }
// 更新成员信息 // 更新成员信息
GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(),session.getId()); GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(),session.getUserId());
if(member == null){ if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群聊的成员"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群聊的成员");
} }
@ -120,8 +116,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
/** /**
* 删除群聊 * 删除群聊
* *
* @Param groupId 群聊id * @param groupId 群聊id
* @return
**/ **/
@Transactional @Transactional
@CacheEvict(value = "#groupId") @CacheEvict(value = "#groupId")
@ -129,7 +124,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void deleteGroup(Long groupId) { public void deleteGroup(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(!group.getOwnerId().equals(session.getId())){ if(!group.getOwnerId().equals(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊");
} }
// 逻辑删除群数据 // 逻辑删除群数据
@ -145,11 +140,10 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
* 退出群聊 * 退出群聊
* *
* @param groupId 群聊id * @param groupId 群聊id
* @return
*/ */
@Override @Override
public void quitGroup(Long groupId) { public void quitGroup(Long groupId) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getUserId();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(group.getOwnerId().equals(userId)){ if(group.getOwnerId().equals(userId)){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊");
@ -165,16 +159,15 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
* *
* @param groupId 群聊id * @param groupId 群聊id
* @param userId 用户id * @param userId 用户id
* @return
*/ */
@Override @Override
public void kickGroup(Long groupId, Long userId) { public void kickGroup(Long groupId, Long userId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(!group.getOwnerId().equals(session.getId())){ if(!group.getOwnerId().equals(session.getUserId()) ){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群主,没有权限踢人"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群主,没有权限踢人");
} }
if(userId.equals(session.getId())){ if(userId.equals(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"亲,不能自己踢自己哟"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"亲,不能自己踢自己哟");
} }
// 删除群聊成员 // 删除群聊成员
@ -186,7 +179,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public GroupVO findById(Long groupId) { public GroupVO findById(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = this.getById(groupId);
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getId()); GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getUserId());
if(member == null){ if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊");
} }
@ -200,11 +193,11 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
* 根据id查找群聊并进行缓存 * 根据id查找群聊并进行缓存
* *
* @param groupId 群聊id * @param groupId 群聊id
* @return * @return 群聊实体
*/ */
@Cacheable(value = "#groupId") @Cacheable(value = "#groupId")
@Override @Override
public Group GetById(Long groupId){ public Group getById(Long groupId){
Group group = super.getById(groupId); Group group = super.getById(groupId);
if(group == null){ if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
@ -220,37 +213,35 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
/** /**
* 查询当前用户的所有群聊 * 查询当前用户的所有群聊
* *
* @return * @return 群聊信息列表
**/ **/
@Override @Override
public List<GroupVO> findGroups() { public List<GroupVO> findGroups() {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 查询当前用户的群id列表 // 查询当前用户的群id列表
List<GroupMember> groupMembers = groupMemberService.findByUserId(session.getId()); List<GroupMember> groupMembers = groupMemberService.findByUserId(session.getUserId());
if(groupMembers.isEmpty()){ if(groupMembers.isEmpty()){
return Collections.EMPTY_LIST; return new LinkedList<>();
} }
// 拉取群列表 // 拉取群列表
List<Long> ids = groupMembers.stream().map((gm -> gm.getGroupId())).collect(Collectors.toList()); List<Long> ids = groupMembers.stream().map((GroupMember::getGroupId)).collect(Collectors.toList());
QueryWrapper<Group> groupWrapper = new QueryWrapper(); LambdaQueryWrapper<Group> groupWrapper = Wrappers.lambdaQuery();
groupWrapper.lambda().in(Group::getId, ids); groupWrapper.in(Group::getId, ids);
List<Group> groups = this.list(groupWrapper); List<Group> groups = this.list(groupWrapper);
// 转vo // 转vo
List<GroupVO> vos = groups.stream().map(g -> { return groups.stream().map(g -> {
GroupVO vo = BeanUtils.copyProperties(g, GroupVO.class); GroupVO vo = BeanUtils.copyProperties(g, GroupVO.class);
GroupMember member = groupMembers.stream().filter(m -> g.getId().equals(m.getGroupId())).findFirst().get(); GroupMember member = groupMembers.stream().filter(m -> g.getId().equals(m.getGroupId())).findFirst().get();
vo.setAliasName(member.getAliasName()); vo.setAliasName(member.getAliasName());
vo.setRemark(member.getRemark()); vo.setRemark(member.getRemark());
return vo; return vo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
return vos;
} }
/** /**
* 邀请好友进群 * 邀请好友进群
* *
* @Param GroupInviteVO 群id好友id列表 * @Param GroupInviteVO 群id好友id列表
* @return
**/ **/
@Override @Override
public void invite(GroupInviteVO vo) { public void invite(GroupInviteVO vo) {
@ -265,9 +256,8 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if(vo.getFriendIds().size() + size > Constant.MAX_GROUP_MEMBER){ 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+"人");
} }
// 找出好友信息 // 找出好友信息
List<Friend> friends = friendsService.findFriendByUserId(session.getId()); List<Friend> friends = friendsService.findFriendByUserId(session.getUserId());
List<Friend> friendsList = vo.getFriendIds().stream().map(id -> List<Friend> friendsList = vo.getFriendIds().stream().map(id ->
friends.stream().filter(f -> f.getFriendId().equals(id)).findFirst().get()).collect(Collectors.toList()); friends.stream().filter(f -> f.getFriendId().equals(id)).findFirst().get()).collect(Collectors.toList());
if (friendsList.size() != vo.getFriendIds().size()) { if (friendsList.size() != vo.getFriendIds().size()) {
@ -276,8 +266,8 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 批量保存成员数据 // 批量保存成员数据
List<GroupMember> groupMembers = friendsList.stream() List<GroupMember> groupMembers = friendsList.stream()
.map(f -> { .map(f -> {
Optional<GroupMember> optional = members.stream().filter(m-> m.getUserId().equals(f.getFriendId())).findFirst(); Optional<GroupMember> optional = members.stream().filter(m->m.getUserId().equals(f.getFriendId())).findFirst();
GroupMember groupMember = optional.isPresent()? optional.get():new GroupMember(); GroupMember groupMember = optional.orElseGet(GroupMember::new);
groupMember.setGroupId(vo.getGroupId()); groupMember.setGroupId(vo.getGroupId());
groupMember.setUserId(f.getFriendId()); groupMember.setUserId(f.getFriendId());
groupMember.setAliasName(f.getFriendNickName()); groupMember.setAliasName(f.getFriendNickName());
@ -302,11 +292,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Override @Override
public List<GroupMemberVO> findGroupMembers(Long groupId) { public List<GroupMemberVO> findGroupMembers(Long groupId) {
List<GroupMember> members = groupMemberService.findByGroupId(groupId); List<GroupMember> members = groupMemberService.findByGroupId(groupId);
List<GroupMemberVO> vos = members.stream().map(m->{ return members.stream().map(m->BeanUtils.copyProperties(m,GroupMemberVO.class)).collect(Collectors.toList());
GroupMemberVO vo = BeanUtils.copyProperties(m,GroupMemberVO.class);
return vo;
}).collect(Collectors.toList());
return vos;
} }
} }

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

@ -1,11 +1,14 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.Constant; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType; import com.bx.implatform.enums.MessageType;
@ -15,13 +18,14 @@ import com.bx.implatform.mapper.PrivateMessageMapper;
import com.bx.implatform.service.IFriendService; import com.bx.implatform.service.IFriendService;
import com.bx.implatform.service.IPrivateMessageService; import com.bx.implatform.service.IPrivateMessageService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.vo.PrivateMessageVO; import com.bx.implatform.dto.PrivateMessageDTO;
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.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,33 +36,37 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Autowired @Autowired
private IFriendService friendService; private IFriendService friendService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired @Autowired
private IMClient imClient; private IMClient imClient;
/** /**
* 发送私聊消息 * 发送私聊消息
* *
* @param vo 私聊消息vo * @param dto 私聊消息
* @return 消息id * @return 消息id
*/ */
@Override @Override
public Long sendMessage(PrivateMessageVO vo) { public Long sendMessage(PrivateMessageDTO dto) {
Long userId = SessionContext.getSession().getId(); UserSession session = SessionContext.getSession();
Boolean isFriends = friendService.isFriend(userId, vo.getRecvId()); Boolean isFriends = friendService.isFriend(session.getUserId(), dto.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(dto, PrivateMessage.class);
msg.setSendId(userId); msg.setSendId(session.getUserId());
msg.setStatus(MessageStatus.UNREAD.code()); msg.setStatus(MessageStatus.UNREAD.code());
msg.setSendTime(new Date()); msg.setSendTime(new Date());
this.save(msg); this.save(msg);
// 推送消息 // 推送消息
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
imClient.sendPrivateMessage(vo.getRecvId(),msgInfo); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", userId, vo.getRecvId(), vo.getContent()); sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(msgInfo.getRecvId());
sendMessage.setSendToSelf(true);
sendMessage.setData(msgInfo);
imClient.sendPrivateMessage(sendMessage);
log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", session.getUserId(), dto.getRecvId(), dto.getContent());
return msg.getId(); return msg.getId();
} }
@ -69,26 +77,39 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
*/ */
@Override @Override
public void recallMessage(Long id) { public void recallMessage(Long id) {
Long userId = SessionContext.getSession().getId(); UserSession session = SessionContext.getSession();
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().equals(userId)) { if (!msg.getSendId().equals(session.getUserId())) {
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() > IMConstant.ALLOW_RECALL_SECOND * 1000) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息已发送超过5分钟,无法撤回"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息已发送超过5分钟,无法撤回");
} }
// 修改消息状态 // 修改消息状态
msg.setStatus(MessageStatus.RECALL.code()); msg.setStatus(MessageStatus.RECALL.code());
this.updateById(msg); this.updateById(msg);
// 推送消息 // 推送消息
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
msgInfo.setType(MessageType.TIP.code()); msgInfo.setType(MessageType.RECALL.code());
msgInfo.setSendTime(new Date()); msgInfo.setSendTime(new Date());
msgInfo.setContent("对方撤回了一条消息"); msgInfo.setContent("对方撤回了一条消息");
imClient.sendPrivateMessage(msgInfo.getRecvId(),msgInfo);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(msgInfo.getRecvId());
sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
// 推给自己其他终端
msgInfo.setContent("你撤回了一条消息");
sendMessage.setSendToSelf(true);
sendMessage.setRecvTerminals(Collections.emptyList());
imClient.sendPrivateMessage(sendMessage);
log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent()); log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent());
} }
@ -102,11 +123,11 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
* @return 聊天记录列表 * @return 聊天记录列表
*/ */
@Override @Override
public List<PrivateMessageInfo> findHistoryMessage(Long friendId, Long page, Long size) { public List<PrivateMessageVO> findHistoryMessage(Long friendId, Long page, Long size) {
page = page > 0 ? page : 1; page = page > 0 ? page : 1;
size = size > 0 ? size : 10; size = size > 0 ? size : 10;
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getUserId();
Long stIdx = (page - 1) * size; long stIdx = (page - 1) * size;
QueryWrapper<PrivateMessage> wrapper = new QueryWrapper<>(); QueryWrapper<PrivateMessage> wrapper = new QueryWrapper<>();
wrapper.lambda().and(wrap -> wrap.and( wrapper.lambda().and(wrap -> wrap.and(
wp -> wp.eq(PrivateMessage::getSendId, userId) wp -> wp.eq(PrivateMessage::getSendId, userId)
@ -118,10 +139,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
.last("limit " + stIdx + "," + size); .last("limit " + stIdx + "," + size);
List<PrivateMessage> messages = this.list(wrapper); List<PrivateMessage> messages = this.list(wrapper);
List<PrivateMessageInfo> messageInfos = messages.stream().map(m -> { List<PrivateMessageVO> messageInfos = messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)).collect(Collectors.toList());
PrivateMessageInfo info = BeanUtils.copyProperties(m, PrivateMessageInfo.class);
return info;
}).collect(Collectors.toList());
log.info("拉取聊天记录,用户id:{},好友id:{},数量:{}", userId, friendId, messageInfos.size()); log.info("拉取聊天记录,用户id:{},好友id:{},数量:{}", userId, friendId, messageInfos.size());
return messageInfos; return messageInfos;
} }
@ -129,30 +147,32 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
/** /**
* 异步拉取私聊消息通过websocket异步推送 * 异步拉取私聊消息通过websocket异步推送
* *
* @return
*/ */
@Override @Override
public void pullUnreadMessage() { public void pullUnreadMessage() {
UserSession session = SessionContext.getSession();
// 获取当前连接的channelId // 获取当前连接的channelId
Long userId = SessionContext.getSession().getId(); if (!imClient.isOnline(session.getUserId())) {
if (!imClient.isOnline(userId)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "用户未建立连接"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "用户未建立连接");
} }
// 获取当前用户所有未读消息 // 获取当前用户所有未读消息
QueryWrapper<PrivateMessage> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<PrivateMessage> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.lambda().eq(PrivateMessage::getRecvId, userId) queryWrapper.eq(PrivateMessage::getRecvId, session.getUserId())
.eq(PrivateMessage::getStatus, MessageStatus.UNREAD); .eq(PrivateMessage::getStatus, MessageStatus.UNREAD);
List<PrivateMessage> messages = this.list(queryWrapper); List<PrivateMessage> messages = this.list(queryWrapper);
// 上传至redis,等待推送 // 上传至redis,等待推送
if (!messages.isEmpty()) { for(PrivateMessage message:messages){
List<PrivateMessageInfo> messageInfos = messages.stream().map(m -> { PrivateMessageVO msgInfo = BeanUtils.copyProperties(message, PrivateMessageVO.class);
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(m, PrivateMessageInfo.class);
return msgInfo;
}).collect(Collectors.toList());
// 推送消息 // 推送消息
PrivateMessageInfo[] infoArr = messageInfos.toArray(new PrivateMessageInfo[messageInfos.size()]); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
imClient.sendPrivateMessage(userId,infoArr); sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
log.info("拉取未读私聊消息,用户id:{},数量:{}", userId, infoArr.length); sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(Collections.singletonList(session.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo);
imClient.sendPrivateMessage(sendMessage);
} }
log.info("拉取未读私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
} }
} }

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

@ -1,11 +1,13 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.RedisKey;
import com.bx.implatform.config.JwtProperties; import com.bx.implatform.config.JwtProperties;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.entity.GroupMember;
import com.bx.implatform.entity.User; import com.bx.implatform.entity.User;
@ -65,7 +67,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
@Override @Override
public LoginVO login(LoginDTO dto) { public LoginVO login(LoginDTO dto) {
User user = findUserByName(dto.getUserName()); User user = this.findUserByUserName(dto.getUserName());
if(null == user){ if(null == user){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"用户不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"用户不存在");
} }
@ -74,6 +76,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
} }
// 生成token // 生成token
UserSession session = BeanUtils.copyProperties(user,UserSession.class); UserSession session = BeanUtils.copyProperties(user,UserSession.class);
session.setUserId(user.getId());
session.setTerminal(dto.getTerminal());
String strJson = JSON.toJSONString(session); String strJson = JSON.toJSONString(session);
String accessToken = JwtUtil.sign(user.getId(),strJson,jwtProperties.getAccessTokenExpireIn(),jwtProperties.getAccessTokenSecret()); String accessToken = JwtUtil.sign(user.getId(),strJson,jwtProperties.getAccessTokenExpireIn(),jwtProperties.getAccessTokenSecret());
String refreshToken = JwtUtil.sign(user.getId(),strJson,jwtProperties.getRefreshTokenExpireIn(),jwtProperties.getRefreshTokenSecret()); String refreshToken = JwtUtil.sign(user.getId(),strJson,jwtProperties.getRefreshTokenExpireIn(),jwtProperties.getRefreshTokenSecret());
@ -85,6 +89,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
return vo; return vo;
} }
/** /**
* 用refreshToken换取新 token * 用refreshToken换取新 token
* *
@ -94,7 +101,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
@Override @Override
public LoginVO refreshToken(String refreshToken) { public LoginVO refreshToken(String refreshToken) {
//验证 token //验证 token
if(JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())){ if(!JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())){
throw new GlobalException("refreshToken无效或已过期"); throw new GlobalException("refreshToken无效或已过期");
} }
String strJson = JwtUtil.getInfo(refreshToken); String strJson = JwtUtil.getInfo(refreshToken);
@ -113,11 +120,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
* 用户注册 * 用户注册
* *
* @param dto 注册dto * @param dto 注册dto
* @return
*/ */
@Override @Override
public void register(RegisterDTO dto) { public void register(RegisterDTO dto) {
User user = findUserByName(dto.getUserName()); User user = this.findUserByUserName(dto.getUserName());
if(null != user){ if(null != user){
throw new GlobalException(ResultCode.USERNAME_ALREADY_REGISTER); throw new GlobalException(ResultCode.USERNAME_ALREADY_REGISTER);
} }
@ -127,6 +133,19 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
log.info("注册用户,用户id:{},用户名:{},昵称:{}",user.getId(),dto.getUserName(),dto.getNickName()); log.info("注册用户,用户id:{},用户名:{},昵称:{}",user.getId(),dto.getUserName(),dto.getNickName());
} }
@Override
public void modifyPassword(ModifyPwdDTO dto) {
UserSession session = SessionContext.getSession();
User user = this.getById(session.getUserId());
if(!passwordEncoder.matches(dto.getOldPassword(),user.getPassword())){
throw new GlobalException("旧密码不正确");
}
user.setPassword(passwordEncoder.encode(dto.getNewPassword()));
this.updateById(user);
log.info("用户修改密码,用户id:{},用户名:{},昵称:{}",user.getId(),user.getUserName(),user.getNickName());
}
/** /**
* 根据用户名查询用户 * 根据用户名查询用户
* *
@ -134,23 +153,23 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
* @return * @return
*/ */
@Override @Override
public User findUserByName(String username) { public User findUserByUserName(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.lambda().eq(User::getUserName,username); queryWrapper.eq(User::getUserName,username);
return this.getOne(queryWrapper); return this.getOne(queryWrapper);
} }
/** /**
* 更新用户信息好友昵称和群聊昵称等冗余信息也会更新 * 更新用户信息好友昵称和群聊昵称等冗余信息也会更新
* *
* @param vo 用户信息vo * @param vo 用户信息vo
* @return
*/ */
@Transactional @Transactional
@Override @Override
public void update(UserVO vo) { public void update(UserVO vo) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
if(!session.getId().equals(vo.getId()) ){ if(!session.getUserId().equals(vo.getId()) ){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许修改其他用户的信息!"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许修改其他用户的信息!");
} }
User user = this.getById(vo.getId()); User user = this.getById(vo.getId());
@ -160,7 +179,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
// 更新好友昵称和头像 // 更新好友昵称和头像
if(!user.getNickName().equals(vo.getNickName()) || !user.getHeadImageThumb().equals(vo.getHeadImageThumb())){ if(!user.getNickName().equals(vo.getNickName()) || !user.getHeadImageThumb().equals(vo.getHeadImageThumb())){
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); QueryWrapper<Friend> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Friend::getFriendId,session.getId()); queryWrapper.lambda().eq(Friend::getFriendId,session.getUserId());
List<Friend> friends = friendService.list(queryWrapper); List<Friend> friends = friendService.list(queryWrapper);
for(Friend friend: friends){ for(Friend friend: friends){
friend.setFriendNickName(vo.getNickName()); friend.setFriendNickName(vo.getNickName());
@ -170,7 +189,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
} }
// 更新群聊中的头像 // 更新群聊中的头像
if(!user.getHeadImageThumb().equals(vo.getHeadImageThumb())){ if(!user.getHeadImageThumb().equals(vo.getHeadImageThumb())){
List<GroupMember> members = groupMemberService.findByUserId(session.getId()); List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
for(GroupMember member:members){ for(GroupMember member:members){
member.setHeadImage(vo.getHeadImageThumb()); member.setHeadImage(vo.getHeadImageThumb());
} }
@ -183,7 +202,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()); log.info("用户信息更新,用户:{}}", user);
} }
@ -195,19 +214,36 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
*/ */
@Override @Override
public List<UserVO> findUserByNickName(String nickname) { public List<UserVO> findUserByNickName(String nickname) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.lambda() queryWrapper.like(User::getNickName,nickname).last("limit 20");
.like(User::getNickName,nickname)
.last("limit 20");
List<User> users = this.list(queryWrapper); List<User> users = this.list(queryWrapper);
List<UserVO> vos = users.stream().map(u-> { return users.stream().map(u-> {
UserVO vo = BeanUtils.copyProperties(u,UserVO.class); UserVO vo = BeanUtils.copyProperties(u,UserVO.class);
vo.setOnline(imClient.isOnline(u.getId())); vo.setOnline(imClient.isOnline(u.getId()));
return vo; return vo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
return vos;
} }
/**
* 根据用户昵称查询用户最多返回20条数据
*
* @param name 用户名或昵称
* @return
*/
@Override
public List<UserVO> findUserByName(String name) {
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.like(User::getUserName,name)
.or()
.like(User::getNickName,name)
.last("limit 20");
List<User> users = this.list(queryWrapper);
return users.stream().map(u-> {
UserVO vo = BeanUtils.copyProperties(u,UserVO.class);
vo.setOnline(imClient.isOnline(u.getId()));
return vo;
}).collect(Collectors.toList());
}
/** /**
* 判断用户是否在线返回在线的用户id列表 * 判断用户是否在线返回在线的用户id列表

245
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java

@ -0,0 +1,245 @@
package com.bx.implatform.service.impl;
import com.bx.imclient.IMClient;
import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.implatform.config.ICEServer;
import com.bx.implatform.config.ICEServerConfig;
import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.service.IWebrtcService;
import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession;
import com.bx.implatform.session.WebrtcSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class WebrtcServiceImpl implements IWebrtcService {
@Autowired
private IMClient imClient;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ICEServerConfig iceServerConfig;
@Override
public void call(Long uid, String offer) {
UserSession session = SessionContext.getSession();
if (!imClient.isOnline(uid)) {
throw new GlobalException("对方目前不在线");
}
// 创建webrtc会话
WebrtcSession webrtcSession = new WebrtcSession();
webrtcSession.setCallerId(session.getUserId());
webrtcSession.setCallerTerminal(session.getTerminal());
String key = getSessionKey(session.getUserId(), uid);
redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
// 向对方所有终端发起呼叫
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_CALL.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
messageInfo.setContent(offer);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void accept(Long uid, @RequestBody String answer) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
// 更新接受者信息
webrtcSession.setAcceptorId(session.getUserId());
webrtcSession.setAcceptorTerminal(session.getTerminal());
String key = getSessionKey(session.getUserId(), uid);
redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
// 向发起人推送接受通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_ACCEPT.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
messageInfo.setContent(answer);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
// 告知其他终端已经接受会话,中止呼叫
sendMessage.setSendToSelf(true);
sendMessage.setSendResult(false);
sendMessage.setRecvTerminals((Collections.singletonList(webrtcSession.getCallerTerminal())));
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void reject(Long uid) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
// 删除会话信息
removeWebrtcSession(uid, session.getUserId());
// 向发起人推送拒绝通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_REJECT.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
// 告知其他终端已经拒绝会话,中止呼叫
sendMessage.setSendToSelf(true);
sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void cancel(Long uid) {
UserSession session = SessionContext.getSession();
// 删除会话信息
removeWebrtcSession(session.getUserId(), uid);
// 向对方所有终端推送取消通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_ACCEPT.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setData(messageInfo);
// 通知对方取消会话
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void failed(Long uid, String reason) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
// 删除会话信息
removeWebrtcSession(uid, session.getUserId());
// 向发起方推送通话失败信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_FAILED.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
// 告知其他终端已经会话失败,中止呼叫
sendMessage.setSendToSelf(true);
sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo);
// 通知对方取消会话
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void leave(Long uid) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
// 删除会话信息
removeWebrtcSession(uid, session.getUserId());
// 向对方推送挂断通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_HANDUP.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
Integer terminal = getTerminalType(uid, webrtcSession);
sendMessage.setRecvTerminals(Collections.singletonList(terminal));
sendMessage.setData(messageInfo);
// 通知对方取消会话
imClient.sendPrivateMessage(sendMessage);
}
@Override
public void candidate(Long uid, String candidate) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
// 向发起方推送同步candidate信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_CANDIDATE.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
messageInfo.setContent(candidate);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setRecvId(uid);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
Integer terminal = getTerminalType(uid, webrtcSession);
sendMessage.setRecvTerminals(Collections.singletonList(terminal));
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
}
@Override
public List<ICEServer> getIceServers() {
return iceServerConfig.getIceServers();
}
private WebrtcSession getWebrtcSession(Long userId, Long uid) {
String key = getSessionKey(userId, uid);
WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key);
if (webrtcSession == null) {
throw new GlobalException("视频通话已结束");
}
return webrtcSession;
}
private void removeWebrtcSession(Long userId, Long uid) {
String key = getSessionKey(userId, uid);
redisTemplate.delete(key);
}
private String getSessionKey(Long id1, Long id2) {
Long minId = id1 > id2 ? id2 : id1;
Long maxId = id1 > id2 ? id1 : id2;
return String.join(":", RedisKey.IM_WEBRTC_SESSION, minId.toString(), maxId.toString());
}
private Integer getTerminalType(Long uid, WebrtcSession webrtcSession) {
if (uid.equals(webrtcSession.getCallerId())) {
return webrtcSession.getCallerTerminal();
}
return webrtcSession.getAcceptorTerminal();
}
}

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

@ -54,7 +54,7 @@ public class FileService {
public String uploadFile(MultipartFile file){ public String uploadFile(MultipartFile file){
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getUserId();
// 大小校验 // 大小校验
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");
@ -71,7 +71,7 @@ public class FileService {
public UploadImageVO uploadImage(MultipartFile file){ public UploadImageVO uploadImage(MultipartFile file){
try { try {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getUserId();
// 大小校验 // 大小校验
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");

13
im-platform/src/main/java/com/bx/implatform/session/UserSession.java

@ -1,11 +1,20 @@
package com.bx.implatform.session; package com.bx.implatform.session;
import com.bx.imcommon.model.IMSessionInfo;
import lombok.Data; import lombok.Data;
@Data @Data
public class UserSession { public class UserSession extends IMSessionInfo {
private Long id; /*
* 用户名称
*/
private String userName; private String userName;
/*
* 用户昵称
*/
private String nickName; private String nickName;
} }

32
im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java

@ -0,0 +1,32 @@
package com.bx.implatform.session;
import com.bx.imcommon.enums.IMTerminalType;
import io.swagger.models.auth.In;
import lombok.Data;
/*
* webrtc 会话信息
* @Author Blue
* @Date 2022/10/21
*/
@Data
public class WebrtcSession {
/**
* 发起者id
*/
private Long callerId;
/**
* 发起者终端类型
*/
private Integer callerTerminal;
/**
* 接受者id
*/
private Long acceptorId;
/**
* 接受者终端类型
*/
private Integer acceptorTerminal;
}

1081
im-platform/src/main/java/com/bx/implatform/util/DateTimeUtils.java

File diff suppressed because it is too large

40
im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java

@ -1,28 +1,42 @@
package com.bx.implatform.vo; package com.bx.implatform.vo;
import io.swagger.annotations.ApiModel; import com.bx.imcommon.serializer.DateToLongSerializer;
import io.swagger.annotations.ApiModelProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty; import java.util.Date;
import javax.validation.constraints.NotNull;
@Data @Data
@ApiModel("群聊消息VO")
public class GroupMessageVO { public class GroupMessageVO {
@NotNull(message="群聊id不可为空") /*
@ApiModelProperty(value = "群聊id") * 消息id
*/
private Long id;
/*
* 群聊id
*/
private Long groupId; private Long groupId;
/*
* 发送者id
*/
private Long sendId;
@Length(max=1024,message = "内容长度不得大于1024") /*
@NotEmpty(message="发送内容不可为空") * 消息内容
@ApiModelProperty(value = "发送内容") */
private String content; private String content;
@NotNull(message="消息类型不可为空") /*
@ApiModelProperty(value = "消息类型") * 消息内容类型 具体枚举值由应用层定义
*/
private Integer type; private Integer type;
/**
* 发送时间
*/
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
} }

3
im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java

@ -12,7 +12,7 @@ import javax.validation.constraints.NotNull;
@ApiModel("群信息VO") @ApiModel("群信息VO")
public class GroupVO { public class GroupVO {
@NotNull(message = "群id不可为空")
@ApiModelProperty(value = "群id") @ApiModelProperty(value = "群id")
private Long id; private Long id;
@ -21,7 +21,6 @@ public class GroupVO {
@ApiModelProperty(value = "群名称") @ApiModelProperty(value = "群名称")
private String name; private String name;
@NotNull(message = "群主id不可为空")
@ApiModelProperty(value = "群主id") @ApiModelProperty(value = "群主id")
private Long ownerId; private Long ownerId;

1
im-platform/src/main/java/com/bx/implatform/vo/LoginVO.java

@ -19,4 +19,5 @@ public class LoginVO {
@ApiModelProperty(value = "refreshToken过期时间(秒)") @ApiModelProperty(value = "refreshToken过期时间(秒)")
private Integer refreshTokenExpiresIn; private Integer refreshTokenExpiresIn;
} }

41
im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java

@ -1,31 +1,42 @@
package com.bx.implatform.vo; package com.bx.implatform.vo;
import com.bx.imcommon.serializer.DateToLongSerializer;
import io.swagger.annotations.ApiModel; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty; import java.util.Date;
import javax.validation.constraints.NotNull;
@Data @Data
@ApiModel("私聊消息VO")
public class PrivateMessageVO { public class PrivateMessageVO {
/*
* 消息id
*/
private long id;
@NotNull(message="接收用户id不可为空") /*
@ApiModelProperty(value = "接收用户id") * 发送者id
private Long recvId; */
private Long sendId;
/*
* 接收者id
*/
private Long recvId;
@Length(max=1024,message = "内容长度不得大于1024") /*
@NotEmpty(message="发送内容不可为空") * 发送内容
@ApiModelProperty(value = "发送内容") */
private String content; private String content;
@NotNull(message="消息类型不可为空") /*
@ApiModelProperty(value = "消息类型") * 消息内容类型 IMCmdType
*/
private Integer type; private Integer type;
/**
* 发送时间
*/
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
} }

6
im-platform/src/main/java/com/bx/implatform/vo/UserVO.java

@ -30,7 +30,10 @@ public class UserVO {
@ApiModelProperty(value = "性别") @ApiModelProperty(value = "性别")
private Integer sex; private Integer sex;
@Length(max = 64,message = "个性签名不能大于1024个字符") @ApiModelProperty(value = "用户类型 1:普通用户 2:审核账户")
private Integer type;
@Length(max = 1024,message = "个性签名不能大于1024个字符")
@ApiModelProperty(value = "个性签名") @ApiModelProperty(value = "个性签名")
private String signature; private String signature;
@ -40,7 +43,6 @@ public class UserVO {
@ApiModelProperty(value = "头像缩略图") @ApiModelProperty(value = "头像缩略图")
private String headImageThumb; private String headImageThumb;
@ApiModelProperty(value = "是否在线") @ApiModelProperty(value = "是否在线")
private Boolean online; private Boolean online;

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

@ -6,7 +6,8 @@ create table `im_user`(
`head_image` varchar(255) default '' comment '用户头像', `head_image` varchar(255) default '' comment '用户头像',
`head_image_thumb` varchar(255) default '' comment '用户头像缩略图', `head_image_thumb` varchar(255) default '' comment '用户头像缩略图',
`password` varchar(255) not null comment '密码(明文)', `password` varchar(255) not null comment '密码(明文)',
`sex` tinyint(1) default 0 comment '性别 0:男 1::女', `sex` tinyint(1) default 0 comment '性别 0:男 1:女',
`type` smallint default 1 comment '用户类型 1:普通用户 2:审核账户',
`signature` varchar(1024) default '' comment '个性签名', `signature` varchar(1024) default '' comment '个性签名',
`last_login_time` datetime DEFAULT null comment '最后登录时间', `last_login_time` datetime DEFAULT null comment '最后登录时间',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',
@ -45,8 +46,8 @@ 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 '是否已删除', `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 '';
create table `im_group_member`( create table `im_group_member`(

1
im-server/src/main/java/com/bx/imserver/IMServerApp.java

@ -19,5 +19,4 @@ public class IMServerApp {
SpringApplication.run(IMServerApp.class,args); SpringApplication.run(IMServerApp.class,args);
} }
} }

23
im-server/src/main/java/com/bx/imserver/config/RedisConfig.java

@ -1,5 +1,6 @@
package com.bx.imserver.config; package com.bx.imserver.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
@ -26,9 +27,9 @@ public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置值(value)的序列化采用jackson2JsonRedisSerializer // 设置值(value)的序列化采用FastJsonRedisSerializer
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(fastJsonRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer());
// 设置键(key)的序列化采用StringRedisSerializer。 // 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer());
@ -36,17 +37,11 @@ public class RedisConfig {
return redisTemplate; return redisTemplate;
} }
@Bean
public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ public FastJsonRedisSerializer fastJsonRedisSerializer(){
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); FastJsonRedisSerializer <Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper(); return fastJsonRedisSerializer;
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 解决jackson2无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
} }
} }

12
im-server/src/main/java/com/bx/imserver/constant/ChannelAttrKey.java

@ -0,0 +1,12 @@
package com.bx.imserver.constant;
public class ChannelAttrKey {
// 用户ID
public static final String USER_ID = "USER_ID";
// 终端类型
public static final String TERMINAL_TYPE = "TERMINAL_TYPE";
// 心跳次数
public static final String HEARTBEAT_TIMES = "HEARTBEAt_TIMES";
}

25
im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java

@ -1,9 +1,10 @@
package com.bx.imserver.netty; package com.bx.imserver.netty;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imserver.netty.processor.MessageProcessor; import com.bx.imserver.constant.ChannelAttrKey;
import com.bx.imserver.netty.processor.AbstractMessageProcessor;
import com.bx.imserver.netty.processor.ProcessorFactory; import com.bx.imserver.netty.processor.ProcessorFactory;
import com.bx.imserver.util.SpringContextHolder; import com.bx.imserver.util.SpringContextHolder;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
@ -33,7 +34,7 @@ public class IMChannelHandler extends SimpleChannelInboundHandler<IMSendInfo> {
@Override @Override
protected void channelRead0(ChannelHandlerContext ctx, IMSendInfo sendInfo) throws Exception { protected void channelRead0(ChannelHandlerContext ctx, IMSendInfo sendInfo) throws Exception {
// 创建处理器进行处理 // 创建处理器进行处理
MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.fromCode(sendInfo.getCmd())); AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.fromCode(sendInfo.getCmd()));
processor.process(ctx,processor.transForm(sendInfo.getData())); processor.process(ctx,processor.transForm(sendInfo.getData()));
} }
@ -64,18 +65,20 @@ public class IMChannelHandler extends SimpleChannelInboundHandler<IMSendInfo> {
@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> userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID);
Long userId = ctx.channel().attr(attr).get(); Long userId = ctx.channel().attr(userIdAttr).get();
ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId); AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
Integer terminal = ctx.channel().attr(terminalAttr).get();
ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId,terminal);
// 判断一下,避免异地登录导致的误删 // 判断一下,避免异地登录导致的误删
if(context != null && ctx.channel().id().equals(context.channel().id())){ if(context != null && ctx.channel().id().equals(context.channel().id())){
// 移除channel // 移除channel
UserChannelCtxMap.removeChannelCtx(userId); UserChannelCtxMap.removeChannelCtx(userId,terminal);
// 用户下线 // 用户下线
RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate"); RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
String key = RedisKey.IM_USER_SERVER_ID + userId; String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID,userId.toString(), terminal.toString());
redisTemplate.delete(key); redisTemplate.delete(key);
log.info("断开连接,userId:{}",userId); log.info("断开连接,userId:{},终端类型:{}",userId,terminal);
} }
} }
@ -87,7 +90,9 @@ public class IMChannelHandler extends SimpleChannelInboundHandler<IMSendInfo> {
// 在规定时间内没有收到客户端的上行数据, 主动断开连接 // 在规定时间内没有收到客户端的上行数据, 主动断开连接
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();
log.info("心跳超时,即将断开连接,用户id:{} ",userId); AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
Integer ternimal = ctx.channel().attr(terminalAttr).get();
log.info("心跳超时,即将断开连接,用户id:{},终端类型:{} ",userId,ternimal);
ctx.channel().close(); ctx.channel().close();
} }
} else { } else {

4
im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java

@ -1,6 +1,6 @@
package com.bx.imserver.netty; package com.bx.imserver.netty;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
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.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
@ -39,7 +39,7 @@ public class IMServerGroup implements CommandLineRunner {
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
// 初始化SERVER_ID // 初始化SERVER_ID
String key = RedisKey.IM_MAX_SERVER_ID; String key = IMRedisKey.IM_MAX_SERVER_ID;
serverId = redisTemplate.opsForValue().increment(key,1); serverId = redisTemplate.opsForValue().increment(key,1);
// 启动服务 // 启动服务
for(IMServer imServer:imServers){ for(IMServer imServer:imServers){

30
im-server/src/main/java/com/bx/imserver/netty/UserChannelCtxMap.java

@ -2,6 +2,7 @@ package com.bx.imserver.netty;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -9,21 +10,34 @@ import java.util.concurrent.ConcurrentHashMap;
public class UserChannelCtxMap { public class UserChannelCtxMap {
/* /*
* 维护userId和ctx的关联关系格式:Map<userId,ctx> * 维护userId和ctx的关联关系格式:Map<userId,map<terminalctx>>
*/ */
private static Map<Long, ChannelHandlerContext> channelMap = new ConcurrentHashMap(); private static Map<Long, Map<Integer,ChannelHandlerContext>> channelMap = new ConcurrentHashMap();
public static void addChannelCtx(Long userId,ChannelHandlerContext ctx){ public static void addChannelCtx(Long userId,Integer channel,ChannelHandlerContext ctx){
channelMap.put(userId,ctx); channelMap.computeIfAbsent(userId,key -> new ConcurrentHashMap()).put(channel,ctx);
} }
public static void removeChannelCtx(Long userId){ public static void removeChannelCtx(Long userId,Integer terminal){
if(userId != null){ if(userId != null && terminal != null && channelMap.containsKey(userId)){
channelMap.remove(userId); Map<Integer,ChannelHandlerContext> userChannelMap = channelMap.get(userId);
if(userChannelMap.containsKey(terminal)){
userChannelMap.remove(terminal);
} }
} }
}
public static ChannelHandlerContext getChannelCtx(Long userId,Integer terminal){
if(userId != null && terminal != null && channelMap.containsKey(userId)){
Map<Integer,ChannelHandlerContext> userChannelMap = channelMap.get(userId);
if(userChannelMap.containsKey(terminal)){
return userChannelMap.get(terminal);
}
}
return null;
}
public static ChannelHandlerContext getChannelCtx(Long userId){ public static Map<Integer,ChannelHandlerContext> getChannelCtx(Long userId){
if(userId == null){ if(userId == null){
return null; return null;
} }

2
im-server/src/main/java/com/bx/imserver/netty/processor/MessageProcessor.java → im-server/src/main/java/com/bx/imserver/netty/processor/AbstractMessageProcessor.java

@ -3,7 +3,7 @@ package com.bx.imserver.netty.processor;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
public abstract class MessageProcessor<T> { public abstract class AbstractMessageProcessor<T> {
public void process(ChannelHandlerContext ctx,T data){} public void process(ChannelHandlerContext ctx,T data){}

62
im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java

@ -1,12 +1,12 @@
package com.bx.imserver.netty.processor; package com.bx.imserver.netty.processor;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMSendCode; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.model.IMSendResult;
import com.bx.imserver.netty.UserChannelCtxMap; import com.bx.imserver.netty.UserChannelCtxMap;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -19,55 +19,53 @@ import java.util.List;
@Slf4j @Slf4j
@Component @Component
public class GroupMessageProcessor extends MessageProcessor<IMRecvInfo<GroupMessageInfo>> { public class GroupMessageProcessor extends AbstractMessageProcessor<IMRecvInfo> {
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@Async @Async
@Override @Override
public void process(IMRecvInfo<GroupMessageInfo> recvInfo) { public void process(IMRecvInfo recvInfo) {
GroupMessageInfo messageInfo = recvInfo.getData(); IMUserInfo sender = recvInfo.getSender();
List<Long> recvIds = recvInfo.getRecvIds(); List<IMUserInfo> receivers = recvInfo.getReceivers();
log.info("接收到群消息,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent()); log.info("接收到群消息,发送者:{},接收用户数量:{},内容:{}",sender.getId(),receivers.size(),recvInfo.getData());
for(Long recvId:recvIds){ for(IMUserInfo receiver:receivers){
try { try {
ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(recvId); ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(receiver.getId(),receiver.getTerminal());
if(channelCtx != null){ if(channelCtx != null){
// 推送消息到用户 // 推送消息到用户
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
sendInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); sendInfo.setCmd(IMCmdType.GROUP_MESSAGE.code());
sendInfo.setData(messageInfo); sendInfo.setData(recvInfo.getData());
channelCtx.channel().writeAndFlush(sendInfo); channelCtx.channel().writeAndFlush(sendInfo);
// 消息发送成功确认 // 消息发送成功确认
String key = RedisKey.IM_RESULT_GROUP_QUEUE; sendResult(recvInfo,receiver,IMSendCode.SUCCESS);
SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.SUCCESS);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
}else { }else {
// 消息发送失败确认 // 消息发送成功确认
String key = RedisKey.IM_RESULT_GROUP_QUEUE; sendResult(recvInfo,receiver,IMSendCode.NOT_FIND_CHANNEL);
SendResult sendResult = new SendResult(); log.error("未找到channel,发送者:{},接收id:{},内容:{}",sender.getId(),receiver.getId(),recvInfo.getData());
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.NOT_FIND_CHANNEL);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
log.error("未找到WS连接,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent());
} }
}catch (Exception e){ }catch (Exception e){
// 消息发送失败确认 // 消息发送失败确认
String key = RedisKey.IM_RESULT_GROUP_QUEUE; sendResult(recvInfo,receiver,IMSendCode.UNKONW_ERROR);
SendResult sendResult = new SendResult(); log.error("发送消息异常,发送者:{},接收id:{},内容:{}",sender.getId(),receiver.getId(),recvInfo.getData());
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.UNKONW_ERROR);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
log.error("发送消息异常,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent());
} }
} }
} }
private void sendResult(IMRecvInfo recvInfo,IMUserInfo receiver,IMSendCode sendCode){
if(recvInfo.getSendResult()) {
IMSendResult result = new IMSendResult();
result.setSender(recvInfo.getSender());
result.setReceiver(receiver);
result.setCode(sendCode.code());
result.setData(recvInfo.getData());
// 推送到结果队列
String key = IMRedisKey.IM_RESULT_GROUP_QUEUE;
redisTemplate.opsForList().rightPush(key, result);
}
}
} }

33
im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java

@ -1,11 +1,12 @@
package com.bx.imserver.netty.processor; package com.bx.imserver.netty.processor;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.bx.imcommon.contant.Constant; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.HeartbeatInfo; import com.bx.imcommon.model.IMHeartbeatInfo;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imserver.constant.ChannelAttrKey;
import com.bx.imserver.netty.ws.WebSocketServer; import com.bx.imserver.netty.ws.WebSocketServer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
@ -19,40 +20,42 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Component @Component
public class HeartbeatProcessor extends MessageProcessor<HeartbeatInfo> { public class HeartbeatProcessor extends AbstractMessageProcessor<IMHeartbeatInfo> {
@Autowired @Autowired
private WebSocketServer WSServer; private WebSocketServer wsServer;
@Autowired @Autowired
RedisTemplate<String,Object> redisTemplate; RedisTemplate<String,Object> redisTemplate;
@Override @Override
public void process(ChannelHandlerContext ctx, HeartbeatInfo beatInfo) { public void process(ChannelHandlerContext ctx, IMHeartbeatInfo beatInfo) {
// 响应ws // 响应ws
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
sendInfo.setCmd(IMCmdType.HEART_BEAT.code()); sendInfo.setCmd(IMCmdType.HEART_BEAT.code());
ctx.channel().writeAndFlush(sendInfo); ctx.channel().writeAndFlush(sendInfo);
// 设置属性 // 设置属性
AttributeKey<Long> attr = AttributeKey.valueOf("HEARTBEAt_TIMES"); AttributeKey<Long> heartBeatAttr = AttributeKey.valueOf(ChannelAttrKey.HEARTBEAT_TIMES);
Long heartbeatTimes = ctx.channel().attr(attr).get(); Long heartbeatTimes = ctx.channel().attr(heartBeatAttr).get();
ctx.channel().attr(attr).set(++heartbeatTimes); ctx.channel().attr(heartBeatAttr).set(++heartbeatTimes);
if(heartbeatTimes%10 == 0){ if(heartbeatTimes%10 == 0){
// 每心跳10次,用户在线状态续一次命 // 每心跳10次,用户在线状态续一次命
attr = AttributeKey.valueOf("USER_ID"); AttributeKey<Long> userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID);
Long userId = ctx.channel().attr(attr).get(); Long userId = ctx.channel().attr(userIdAttr).get();
String key = RedisKey.IM_USER_SERVER_ID+userId; AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
redisTemplate.expire(key, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); Integer ternimal = ctx.channel().attr(terminalAttr).get();
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID,userId.toString(),ternimal.toString());
redisTemplate.expire(key, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS);
} }
} }
@Override @Override
public HeartbeatInfo transForm(Object o) { public IMHeartbeatInfo transForm(Object o) {
HashMap map = (HashMap)o; HashMap map = (HashMap)o;
HeartbeatInfo heartbeatInfo = BeanUtil.fillBeanWithMap(map, new HeartbeatInfo(), false); IMHeartbeatInfo heartbeatInfo = BeanUtil.fillBeanWithMap(map, new IMHeartbeatInfo(), false);
return heartbeatInfo; return heartbeatInfo;
} }
} }

48
im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java

@ -1,15 +1,17 @@
package com.bx.imserver.netty.processor; package com.bx.imserver.netty.processor;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.bx.imcommon.contant.Constant; import com.alibaba.fastjson.JSON;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imcommon.model.LoginInfo; import com.bx.imcommon.model.IMSessionInfo;
import com.bx.imcommon.model.IMLoginInfo;
import com.bx.imcommon.util.JwtUtil; import com.bx.imcommon.util.JwtUtil;
import com.bx.imserver.constant.ChannelAttrKey;
import com.bx.imserver.netty.IMServerGroup; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.UserChannelCtxMap; import com.bx.imserver.netty.UserChannelCtxMap;
import com.bx.imserver.netty.ws.WebSocketServer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -23,11 +25,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Component @Component
public class LoginProcessor extends MessageProcessor<LoginInfo> { public class LoginProcessor extends AbstractMessageProcessor<IMLoginInfo> {
@Autowired
private WebSocketServer WSServer;
@Autowired @Autowired
RedisTemplate<String,Object> redisTemplate; RedisTemplate<String,Object> redisTemplate;
@ -36,14 +34,17 @@ public class LoginProcessor extends MessageProcessor<LoginInfo> {
private String accessTokenSecret; private String accessTokenSecret;
@Override @Override
synchronized public void process(ChannelHandlerContext ctx, LoginInfo loginInfo) { synchronized public void process(ChannelHandlerContext ctx, IMLoginInfo loginInfo) {
if(!JwtUtil.checkSign(loginInfo.getAccessToken(),accessTokenSecret)){ if(!JwtUtil.checkSign(loginInfo.getAccessToken(),accessTokenSecret)){
ctx.channel().close(); ctx.channel().close();
log.warn("用户token校验不通过,强制下线,token:{}",loginInfo.getAccessToken()); log.warn("用户token校验不通过,强制下线,token:{}",loginInfo.getAccessToken());
} }
Long userId = JwtUtil.getUserId(loginInfo.getAccessToken()); String strInfo = JwtUtil.getInfo(loginInfo.getAccessToken());
IMSessionInfo sessionInfo = JSON.parseObject(strInfo,IMSessionInfo.class);
Long userId = sessionInfo.getUserId();
Integer terminal = sessionInfo.getTerminal();
log.info("用户登录,userId:{}",userId); log.info("用户登录,userId:{}",userId);
ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId); ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId,terminal);
if(context != null && !ctx.channel().id().equals(context.channel().id())){ if(context != null && !ctx.channel().id().equals(context.channel().id())){
// 不允许多地登录,强制下线 // 不允许多地登录,强制下线
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
@ -53,16 +54,19 @@ public class LoginProcessor extends MessageProcessor<LoginInfo> {
log.info("异地登录,强制下线,userId:{}",userId); log.info("异地登录,强制下线,userId:{}",userId);
} }
// 绑定用户和channel // 绑定用户和channel
UserChannelCtxMap.addChannelCtx(userId,ctx); UserChannelCtxMap.addChannelCtx(userId,terminal,ctx);
// 设置用户id属性 // 设置用户id属性
AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID"); AttributeKey<Long> userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID);
ctx.channel().attr(attr).set(userId); ctx.channel().attr(userIdAttr).set(userId);
// 心跳次数 // 设置用户终端类型
attr = AttributeKey.valueOf("HEARTBEAt_TIMES"); AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
ctx.channel().attr(attr).set(0L); ctx.channel().attr(terminalAttr).set(terminal);
// 初始化心跳次数
AttributeKey<Long> heartBeatAttr = AttributeKey.valueOf("HEARTBEAt_TIMES");
ctx.channel().attr(heartBeatAttr).set(0L);
// 在redis上记录每个user的channelId,15秒没有心跳,则自动过期 // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期
String key = RedisKey.IM_USER_SERVER_ID+userId; String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID,userId.toString(), terminal.toString());
redisTemplate.opsForValue().set(key, IMServerGroup.serverId, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); redisTemplate.opsForValue().set(key, IMServerGroup.serverId, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS);
// 响应ws // 响应ws
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
sendInfo.setCmd(IMCmdType.LOGIN.code()); sendInfo.setCmd(IMCmdType.LOGIN.code());
@ -71,9 +75,9 @@ public class LoginProcessor extends MessageProcessor<LoginInfo> {
@Override @Override
public LoginInfo transForm(Object o) { public IMLoginInfo transForm(Object o) {
HashMap map = (HashMap)o; HashMap map = (HashMap)o;
LoginInfo loginInfo = BeanUtil.fillBeanWithMap(map, new LoginInfo(), false); IMLoginInfo loginInfo = BeanUtil.fillBeanWithMap(map, new IMLoginInfo(), false);
return loginInfo; return loginInfo;
} }
} }

57
im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java

@ -1,12 +1,12 @@
package com.bx.imserver.netty.processor; package com.bx.imserver.netty.processor;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMSendCode; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.IMSendResult;
import com.bx.imserver.netty.UserChannelCtxMap; import com.bx.imserver.netty.UserChannelCtxMap;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -16,52 +16,49 @@ import org.springframework.stereotype.Component;
@Slf4j @Slf4j
@Component @Component
public class PrivateMessageProcessor extends MessageProcessor<IMRecvInfo<PrivateMessageInfo>> { public class PrivateMessageProcessor extends AbstractMessageProcessor<IMRecvInfo> {
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@Override @Override
public void process(IMRecvInfo<PrivateMessageInfo> recvInfo) { public void process(IMRecvInfo recvInfo) {
PrivateMessageInfo messageInfo = recvInfo.getData(); IMUserInfo sender = recvInfo.getSender();
Long recvId = recvInfo.getRecvIds().get(0); IMUserInfo receiver = recvInfo.getReceivers().get(0);
log.info("接收到消息,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent()); log.info("接收到消息,发送者:{},接收者:{},内容:{}",sender.getId(),receiver.getId(),recvInfo.getData());
try{ try{
ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(recvId); ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(receiver.getId(),receiver.getTerminal());
if(channelCtx != null ){ if(channelCtx != null ){
// 推送消息到用户 // 推送消息到用户
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
sendInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); sendInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code());
sendInfo.setData(messageInfo); sendInfo.setData(recvInfo.getData());
channelCtx.channel().writeAndFlush(sendInfo); channelCtx.channel().writeAndFlush(sendInfo);
// 消息发送成功确认 // 消息发送成功确认
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; sendResult(recvInfo,IMSendCode.SUCCESS);
SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.SUCCESS);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
}else{ }else{
// 消息推送失败确认 // 消息推送失败确认
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; sendResult(recvInfo,IMSendCode.NOT_FIND_CHANNEL);
SendResult sendResult = new SendResult(); log.error("未找到channel,发送者:{},接收者:{},内容:{}",sender.getId(),receiver.getId(),recvInfo.getData());
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.NOT_FIND_CHANNEL);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
log.error("未找到WS连接,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent());
} }
}catch (Exception e){ }catch (Exception e){
// 消息推送失败确认 // 消息推送失败确认
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; sendResult(recvInfo,IMSendCode.UNKONW_ERROR);
SendResult sendResult = new SendResult(); log.error("发送异常,发送者:{},接收者:{},内容:{}",sender.getId(),receiver.getId(),recvInfo.getData(),e);
sendResult.setRecvId(recvId);
sendResult.setCode(IMSendCode.UNKONW_ERROR);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
log.error("发送异常,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent(),e);
} }
} }
private void sendResult(IMRecvInfo recvInfo,IMSendCode sendCode){
if(recvInfo.getSendResult()) {
IMSendResult result = new IMSendResult();
result.setSender(recvInfo.getSender());
result.setReceiver(recvInfo.getReceivers().get(0));
result.setCode(sendCode.code());
result.setData(recvInfo.getData());
// 推送到结果队列
String key = IMRedisKey.IM_RESULT_PRIVATE_QUEUE;
redisTemplate.opsForList().rightPush(key, result);
}
}
} }

12
im-server/src/main/java/com/bx/imserver/netty/processor/ProcessorFactory.java

@ -5,20 +5,20 @@ import com.bx.imserver.util.SpringContextHolder;
public class ProcessorFactory { public class ProcessorFactory {
public static MessageProcessor createProcessor(IMCmdType cmd){ public static AbstractMessageProcessor createProcessor(IMCmdType cmd){
MessageProcessor processor = null; AbstractMessageProcessor processor = null;
switch (cmd){ switch (cmd){
case LOGIN: case LOGIN:
processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class); processor = (AbstractMessageProcessor) SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class);
break; break;
case HEART_BEAT: case HEART_BEAT:
processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class); processor = (AbstractMessageProcessor) SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class);
break; break;
case PRIVATE_MESSAGE: case PRIVATE_MESSAGE:
processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(PrivateMessageProcessor.class); processor = (AbstractMessageProcessor)SpringContextHolder.getApplicationContext().getBean(PrivateMessageProcessor.class);
break; break;
case GROUP_MESSAGE: case GROUP_MESSAGE:
processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(GroupMessageProcessor.class); processor = (AbstractMessageProcessor)SpringContextHolder.getApplicationContext().getBean(GroupMessageProcessor.class);
break; break;
default: default:
break; break;

2
im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java

@ -59,7 +59,7 @@ public class TcpSocketServer implements IMServer {
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
// 获取职责链 // 获取职责链
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("encode",new MessageProtocolEncoder()); pipeline.addLast("encode",new MessageProtocolEncoder());
pipeline.addLast("decode",new MessageProtocolDecoder()); pipeline.addLast("decode",new MessageProtocolDecoder());
pipeline.addLast("handler", new IMChannelHandler()); pipeline.addLast("handler", new IMChannelHandler());

2
im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java

@ -63,7 +63,7 @@ public class WebSocketServer implements IMServer {
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
// 获取职责链 // 获取职责链
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("http-codec", new HttpServerCodec()); pipeline.addLast("http-codec", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); pipeline.addLast("aggregator", new HttpObjectAggregator(65535));
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); pipeline.addLast("http-chunked", new ChunkedWriteHandler());

28
im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java

@ -7,11 +7,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService; import java.util.concurrent.*;
import java.util.concurrent.Executors;
@Slf4j @Slf4j
public abstract class AbstractPullMessageTask{ public abstract class AbstractPullMessageTask {
private int threadNum = 1; private int threadNum = 1;
private ExecutorService executorService; private ExecutorService executorService;
@ -19,34 +18,33 @@ public abstract class AbstractPullMessageTask{
@Autowired @Autowired
private IMServerGroup serverGroup; private IMServerGroup serverGroup;
public AbstractPullMessageTask(){ public AbstractPullMessageTask() {
this.threadNum = 1; this.threadNum = 1;
} }
public AbstractPullMessageTask(int threadNum){ public AbstractPullMessageTask(int threadNum) {
this.threadNum = threadNum; this.threadNum = threadNum;
} }
@PostConstruct @PostConstruct
public void init(){ public void init() {
// 初始化定时器 // 初始化定时器
executorService = Executors.newFixedThreadPool(threadNum); executorService = Executors.newFixedThreadPool(threadNum);
for(int i=0;i<threadNum;i++){ for (int i = 0; i < threadNum; i++) {
executorService.execute(new Runnable() { executorService.execute(new Runnable() {
@SneakyThrows @SneakyThrows
@Override @Override
public void run() { public void run() {
try{ try {
if(serverGroup.isReady()){ if (serverGroup.isReady()) {
pullMessage(); pullMessage();
} }
Thread.sleep(100); } catch (Exception e) {
}catch (Exception e){ log.error("任务调度异常", e);
log.error("任务调度异常",e);
Thread.sleep(200); Thread.sleep(200);
} }
if(!executorService.isShutdown()){ if (!executorService.isShutdown()) {
executorService.execute(this); executorService.execute(this);
} }
} }
@ -55,8 +53,8 @@ public abstract class AbstractPullMessageTask{
} }
@PreDestroy @PreDestroy
public void destroy(){ public void destroy() {
log.info("{}线程任务关闭",this.getClass().getSimpleName()); log.info("{}线程任务关闭", this.getClass().getSimpleName());
executorService.shutdown(); executorService.shutdown();
} }

28
im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java

@ -1,45 +1,35 @@
package com.bx.imserver.task; package com.bx.imserver.task;
import com.bx.imcommon.contant.RedisKey; import com.alibaba.fastjson.JSONObject;
import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imserver.netty.IMServerGroup; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.processor.MessageProcessor; import com.bx.imserver.netty.processor.AbstractMessageProcessor;
import com.bx.imserver.netty.processor.ProcessorFactory; import com.bx.imserver.netty.processor.ProcessorFactory;
import com.bx.imserver.netty.ws.WebSocketServer;
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.concurrent.TimeUnit;
import java.util.List;
@Slf4j @Slf4j
@Component @Component
public class PullUnreadGroupMessageTask extends AbstractPullMessageTask { public class PullUnreadGroupMessageTask extends AbstractPullMessageTask {
@Autowired
private WebSocketServer WSServer;
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@Override @Override
public void pullMessage() { public void pullMessage() {
// 从redis拉取未读消息 // 从redis拉取未读消息
String key = RedisKey.IM_UNREAD_GROUP_QUEUE + IMServerGroup.serverId; String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE,IMServerGroup.serverId+"");
List messageInfos = redisTemplate.opsForList().range(key,0,-1); JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
for(Object o: messageInfos){ if(jsonObject != null){
redisTemplate.opsForList().leftPop(key); IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);
IMRecvInfo<GroupMessageInfo> recvInfo = (IMRecvInfo)o; AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.GROUP_MESSAGE);
MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.GROUP_MESSAGE);
processor.process(recvInfo); processor.process(recvInfo);
} }
} }
} }

25
im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java

@ -1,28 +1,23 @@
package com.bx.imserver.task; package com.bx.imserver.task;
import com.bx.imcommon.contant.RedisKey; import com.alibaba.fastjson.JSONObject;
import com.bx.imcommon.contant.IMRedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.imserver.netty.IMServerGroup; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.processor.MessageProcessor; import com.bx.imserver.netty.processor.AbstractMessageProcessor;
import com.bx.imserver.netty.processor.ProcessorFactory; import com.bx.imserver.netty.processor.ProcessorFactory;
import com.bx.imserver.netty.ws.WebSocketServer;
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.concurrent.TimeUnit;
import java.util.List;
@Slf4j @Slf4j
@Component @Component
public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask { public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask {
@Autowired
private WebSocketServer WSServer;
@Autowired @Autowired
private RedisTemplate<String,Object> redisTemplate; private RedisTemplate<String,Object> redisTemplate;
@ -30,14 +25,12 @@ public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask {
@Override @Override
public void pullMessage() { public void pullMessage() {
// 从redis拉取未读消息 // 从redis拉取未读消息
String key = RedisKey.IM_UNREAD_PRIVATE_QUEUE + IMServerGroup.serverId; String key = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE ,IMServerGroup.serverId+"");
List messageInfos = redisTemplate.opsForList().range(key,0,-1); JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
for(Object o: messageInfos){ if(jsonObject!=null){
redisTemplate.opsForList().leftPop(key); IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);
IMRecvInfo<PrivateMessageInfo> recvInfo = (IMRecvInfo)o; AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.PRIVATE_MESSAGE);
MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.PRIVATE_MESSAGE);
processor.process(recvInfo); processor.process(recvInfo);
} }
} }

BIN
im-ui/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

2
im-ui/public/index.html

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>logo.png">
<title>盒子IM</title> <title>盒子IM</title>
</head> </head>
<body> <body>

BIN
im-ui/public/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

6
im-ui/src/api/enums.js

@ -1,5 +1,11 @@
const MESSAGE_TYPE = { const MESSAGE_TYPE = {
TEXT: 0,
IMAGE:1,
FILE:2,
AUDIO:3,
VIDEO:4,
RECALL:10,
RTC_CALL: 101, RTC_CALL: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save