├── .gitignore ├── LICENSE ├── README.md ├── im-client ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── im │ │ └── client │ │ ├── IMAutoConfiguration.java │ │ ├── IMClient.java │ │ ├── annotation │ │ └── IMListener.java │ │ ├── config │ │ └── RedisConfig.java │ │ ├── listener │ │ ├── MessageListener.java │ │ └── MessageListenerMulticaster.java │ │ ├── sender │ │ └── IMSender.java │ │ └── task │ │ ├── AbstractPullMessageTask.java │ │ ├── PullSendResultGroupMessageTask.java │ │ └── PullSendResultPrivateMessageTask.java │ └── resources │ └── META-INF │ └── spring.factories ├── im-commom ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── im │ └── common │ ├── contant │ ├── Constant.java │ └── RedisKey.java │ ├── enums │ ├── IMCmdType.java │ ├── IMListenerType.java │ └── IMSendCode.java │ ├── model │ ├── GroupMessageInfo.java │ ├── HeartbeatInfo.java │ ├── IMRecvInfo.java │ ├── IMSendInfo.java │ ├── LoginInfo.java │ ├── PrivateMessageInfo.java │ └── SendResult.java │ └── serializer │ └── DateToLongSerializer.java ├── im-platform ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── im │ │ └── platform │ │ ├── ImplatformApp.java │ │ ├── config │ │ ├── GlobalCorsConfig.java │ │ ├── ICEServer.java │ │ ├── ICEServerConfig.java │ │ ├── MinIoClientConfig.java │ │ ├── MvcConfig.java │ │ ├── RedisConfig.java │ │ └── SwaggerConfig.java │ │ ├── contant │ │ ├── Constant.java │ │ └── RedisKey.java │ │ ├── controller │ │ ├── FileController.java │ │ ├── FriendController.java │ │ ├── GroupController.java │ │ ├── GroupMessageController.java │ │ ├── LoginController.java │ │ ├── PrivateMessageController.java │ │ ├── UserController.java │ │ └── WebrtcController.java │ │ ├── dto │ │ ├── LoginDTO.java │ │ └── RegisterDTO.java │ │ ├── entity │ │ ├── Friend.java │ │ ├── Group.java │ │ ├── GroupMember.java │ │ ├── GroupMessage.java │ │ ├── PrivateMessage.java │ │ └── User.java │ │ ├── enums │ │ ├── FileType.java │ │ ├── MessageStatus.java │ │ ├── MessageType.java │ │ └── ResultCode.java │ │ ├── exception │ │ ├── GlobalException.java │ │ └── GlobalExceptionHandler.java │ │ ├── generator │ │ └── CodeGenerator.java │ │ ├── interceptor │ │ └── AuthInterceptor.java │ │ ├── listener │ │ ├── GroupMessageListener.java │ │ └── PrivateMessageListener.java │ │ ├── mapper │ │ ├── FriendMapper.java │ │ ├── GroupMapper.java │ │ ├── GroupMemberMapper.java │ │ ├── GroupMessageMapper.java │ │ ├── PrivateMessageMapper.java │ │ └── UserMapper.java │ │ ├── result │ │ ├── Result.java │ │ └── ResultUtils.java │ │ ├── service │ │ ├── IFriendService.java │ │ ├── IGroupMemberService.java │ │ ├── IGroupMessageService.java │ │ ├── IGroupService.java │ │ ├── IPrivateMessageService.java │ │ ├── IUserService.java │ │ ├── impl │ │ │ ├── FriendServiceImpl.java │ │ │ ├── GroupMemberServiceImpl.java │ │ │ ├── GroupMessageServiceImpl.java │ │ │ ├── GroupServiceImpl.java │ │ │ ├── PrivateMessageServiceImpl.java │ │ │ └── UserServiceImpl.java │ │ └── thirdparty │ │ │ └── FileService.java │ │ ├── session │ │ ├── SessionContext.java │ │ └── UserSession.java │ │ ├── util │ │ ├── BeanUtils.java │ │ ├── DateTimeUtils.java │ │ ├── FileUtil.java │ │ ├── ImageUtil.java │ │ ├── JwtUtil.java │ │ └── MinioUtil.java │ │ └── vo │ │ ├── FriendVO.java │ │ ├── GroupInviteVO.java │ │ ├── GroupMemberVO.java │ │ ├── GroupMessageVO.java │ │ ├── GroupVO.java │ │ ├── LoginVO.java │ │ ├── PrivateMessageVO.java │ │ ├── UploadImageVO.java │ │ └── UserVO.java │ └── resources │ ├── application.yml │ └── db │ └── db.sql ├── im-server ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── im │ │ └── server │ │ ├── IMServerApp.java │ │ ├── config │ │ └── RedisConfig.java │ │ ├── netty │ │ ├── IMChannelHandler.java │ │ ├── IMServer.java │ │ ├── IMServerGroup.java │ │ ├── UserChannelCtxMap.java │ │ ├── processor │ │ │ ├── GroupMessageProcessor.java │ │ │ ├── HeartbeatProcessor.java │ │ │ ├── LoginProcessor.java │ │ │ ├── MessageProcessor.java │ │ │ ├── PrivateMessageProcessor.java │ │ │ └── ProcessorFactory.java │ │ ├── tcp │ │ │ ├── TcpSocketServer.java │ │ │ └── endecode │ │ │ │ ├── MessageProtocolDecoder.java │ │ │ │ └── MessageProtocolEncoder.java │ │ └── ws │ │ │ ├── WebSocketServer.java │ │ │ └── endecode │ │ │ ├── MessageProtocolDecoder.java │ │ │ └── MessageProtocolEncoder.java │ │ ├── task │ │ ├── AbstractPullMessageTask.java │ │ ├── PullUnreadGroupMessageTask.java │ │ └── PullUnreadPrivateMessageTask.java │ │ └── util │ │ └── SpringContextHolder.java │ └── resources │ └── application.yml ├── im-ui ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── element.js │ │ ├── emotion.js │ │ ├── enums.js │ │ ├── httpRequest.js │ │ └── wssocket.js │ ├── assets │ │ ├── audio │ │ │ ├── call.wav │ │ │ └── tip.wav │ │ ├── default_head.png │ │ ├── emoji │ │ │ ├── 0.gif │ │ │ ├── 1.gif │ │ │ ├── 10.gif │ │ │ ├── 100.gif │ │ │ ├── 101.gif │ │ │ ├── 102.gif │ │ │ ├── 103.gif │ │ │ ├── 104.gif │ │ │ ├── 11.gif │ │ │ ├── 12.gif │ │ │ ├── 13.gif │ │ │ ├── 14.gif │ │ │ ├── 15.gif │ │ │ ├── 16.gif │ │ │ ├── 17.gif │ │ │ ├── 18.gif │ │ │ ├── 19.gif │ │ │ ├── 2.gif │ │ │ ├── 20.gif │ │ │ ├── 21.gif │ │ │ ├── 22.gif │ │ │ ├── 23.gif │ │ │ ├── 24.gif │ │ │ ├── 25.gif │ │ │ ├── 26.gif │ │ │ ├── 27.gif │ │ │ ├── 28.gif │ │ │ ├── 29.gif │ │ │ ├── 3.gif │ │ │ ├── 30.gif │ │ │ ├── 31.gif │ │ │ ├── 32.gif │ │ │ ├── 33.gif │ │ │ ├── 34.gif │ │ │ ├── 35.gif │ │ │ ├── 36.gif │ │ │ ├── 37.gif │ │ │ ├── 38.gif │ │ │ ├── 39.gif │ │ │ ├── 4.gif │ │ │ ├── 40.gif │ │ │ ├── 41.gif │ │ │ ├── 42.gif │ │ │ ├── 43.gif │ │ │ ├── 44.gif │ │ │ ├── 45.gif │ │ │ ├── 46.gif │ │ │ ├── 47.gif │ │ │ ├── 48.gif │ │ │ ├── 49.gif │ │ │ ├── 5.gif │ │ │ ├── 50.gif │ │ │ ├── 51.gif │ │ │ ├── 52.gif │ │ │ ├── 53.gif │ │ │ ├── 54.gif │ │ │ ├── 55.gif │ │ │ ├── 56.gif │ │ │ ├── 57.gif │ │ │ ├── 58.gif │ │ │ ├── 59.gif │ │ │ ├── 6.gif │ │ │ ├── 60.gif │ │ │ ├── 61.gif │ │ │ ├── 62.gif │ │ │ ├── 63.gif │ │ │ ├── 64.gif │ │ │ ├── 65.gif │ │ │ ├── 66.gif │ │ │ ├── 67.gif │ │ │ ├── 68.gif │ │ │ ├── 69.gif │ │ │ ├── 7.gif │ │ │ ├── 70.gif │ │ │ ├── 71.gif │ │ │ ├── 72.gif │ │ │ ├── 73.gif │ │ │ ├── 74.gif │ │ │ ├── 75.gif │ │ │ ├── 76.gif │ │ │ ├── 77.gif │ │ │ ├── 78.gif │ │ │ ├── 79.gif │ │ │ ├── 8.gif │ │ │ ├── 80.gif │ │ │ ├── 81.gif │ │ │ ├── 82.gif │ │ │ ├── 83.gif │ │ │ ├── 84.gif │ │ │ ├── 85.gif │ │ │ ├── 86.gif │ │ │ ├── 87.gif │ │ │ ├── 88.gif │ │ │ ├── 89.gif │ │ │ ├── 9.gif │ │ │ ├── 90.gif │ │ │ ├── 91.gif │ │ │ ├── 92.gif │ │ │ ├── 93.gif │ │ │ ├── 94.gif │ │ │ ├── 95.gif │ │ │ ├── 96.gif │ │ │ ├── 97.gif │ │ │ ├── 98.gif │ │ │ └── 99.gif │ │ ├── iconfont │ │ │ ├── iconfont.css │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── logo.png │ │ └── style │ │ │ └── global.css │ ├── components │ │ ├── chat │ │ │ ├── ChatBox.vue │ │ │ ├── ChatGroupSide.vue │ │ │ ├── ChatHistory.vue │ │ │ ├── ChatItem.vue │ │ │ ├── ChatMessageItem.vue │ │ │ ├── ChatPrivateVideo.vue │ │ │ ├── ChatTime.vue │ │ │ ├── ChatVideoAcceptor.vue │ │ │ └── ChatVoice.vue │ │ ├── common │ │ │ ├── Emotion.vue │ │ │ ├── FileUpload.vue │ │ │ ├── FullImage.vue │ │ │ ├── HeadImage.vue │ │ │ ├── RightMenu.vue │ │ │ └── UserInfo.vue │ │ ├── friend │ │ │ ├── AddFriend.vue │ │ │ └── FriendItem.vue │ │ ├── group │ │ │ ├── AddGroupMember.vue │ │ │ ├── GroupItem.vue │ │ │ └── GroupMember.vue │ │ └── setting │ │ │ └── Setting.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── chatStore.js │ │ ├── friendStore.js │ │ ├── groupStore.js │ │ ├── index.js │ │ ├── uiStore.js │ │ └── userStore.js │ ├── utils │ │ └── directive │ │ │ └── dialogDrag.js │ └── view │ │ ├── Chat.vue │ │ ├── Friend.vue │ │ ├── Group.vue │ │ ├── Home.vue │ │ ├── Login.vue │ │ └── Register.vue └── vue.config.js ├── pic ├── im_pull.png └── im_push.png └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /box-im.iml 3 | /im-server/im-server.iml 4 | /im-platform/im-platform.iml 5 | /im-platform/src/main/resources/application-prod.yml 6 | /im-platform/src/main/resources/logback-prod.xml 7 | /im-server/src/main/resources/application-prod.yml 8 | /im-server/src/main/resources/logback-prod.xml 9 | /im-commom/im-commom.iml 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 blue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | #### 项目介绍 3 | 1. IM是一个分布式聊天系统,目前完全开源,仅用于学习和交流。 4 | 1. 支持私聊、群聊、离线消息、发送图片、文件、好友在线状态显示等功能。 5 | 1. 后端采用springboot+netty实现,前端使用vue。 6 | 1. 服务器支持集群化部署,每个im-server仅处理自身连接用户的消息 7 | 8 | 9 | 10 | #### 项目结构 11 | | 模块 | 功能 | 12 | |-------------|------------| 13 | | im-platform | 与页面进行交互,处理业务请求 | 14 | | im-server | 推送聊天消息| 15 | | im-client | 消息推送sdk| 16 | | im-common | 公共包 | 17 | 18 | #### 消息推送方案(推方案) 19 | 20 | ![输入图片说明](pic/im_push.png) 21 | 22 | - im通过长连接实现消息推送,单机情况下不同用户的channel是在同一台机器上可以找到并且投递,当场景转换为分布式后,不同的用户channel可能是不同的server在维护,我们需要考虑如何将消息跨server进行投递 23 | - 利用了redis的list数据实现消息推送,其中key为im:unread:${serverid},每个key的数据可以看做一个messageQueue,每个server根据自身的serverId只消费属于自己的queue 24 | - 同时使用一个中心化存储记录了每个用户的websocket连接的serverId,当用户发送消息时,platform将根据receId所连接的server的id,决定将消息推向哪个queue 25 | - 每个server会维护本地的channel,收到messageQueue中的消息后找到对应的Queue进行投递 26 | 27 | #### 热点群聊优化方案(推拉结合) 28 | 29 | ![输入图片说明](pic/im_pull.png) 30 | - 在客户端会维护热点群聊的已读offset,用户发送热点群聊消息给server 31 | 32 | - server统一将消息通过MQ与TS服务进行解耦,TS服务负责将消息进行入库,同时对比用户存在redis中的已读消息的存根是否有必要将message投递到receId对应的messageQueue 33 | 34 | - 若投递到messageQueue后,server消费后投递给client无状态的可以拉取请求 35 | 36 | - client收到请求后进行批量拉取,拉取是需要从DB中拉取,防止DB压力过大,用存根offset与group最新消息进行判断是否有必要拉取。 37 | 38 | - 拉取操作务必使用异步,可以使用MQ,方便可以使用业务线程,防止单个拉取动作过慢导致work线程阻塞进而影响用户的心跳检测。 39 | 40 | -------------------------------------------------------------------------------- /im-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | im 7 | com.im 8 | 1.1.0 9 | 10 | 4.0.0 11 | 12 | im-client 13 | 14 | 15 | 16 | com.im 17 | im-commom 18 | 1.1.0 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-redis 24 | 25 | 26 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/IMAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.im.client; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | 8 | @Slf4j 9 | @Configuration 10 | @ComponentScan("com.bx.imclient") 11 | public class IMAutoConfiguration { 12 | } 13 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/IMClient.java: -------------------------------------------------------------------------------- 1 | package com.im.client; 2 | 3 | import com.im.client.listener.MessageListenerMulticaster; 4 | import com.im.client.sender.IMSender; 5 | import com.im.common.model.GroupMessageInfo; 6 | import com.im.common.model.PrivateMessageInfo; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.util.List; 11 | 12 | @Configuration 13 | public class IMClient { 14 | 15 | @Autowired 16 | private MessageListenerMulticaster listenerMulticaster; 17 | @Autowired 18 | private IMSender imSender; 19 | 20 | /** 21 | * 发送私聊消息 22 | * 23 | * @param recvId 接收用户id 24 | * @param messageInfo 消息体,将转成json发送到客户端 25 | */ 26 | public void sendPrivateMessage(Long recvId, PrivateMessageInfo... messageInfo){ 27 | imSender.sendPrivateMessage(recvId,messageInfo); 28 | } 29 | 30 | /** 31 | * 发送群聊消息 32 | * 33 | * @param recvIds 群聊用户id列表 34 | * @param messageInfo 消息体,将转成json发送到客户端 35 | */ 36 | public void sendGroupMessage(List recvIds, GroupMessageInfo... messageInfo){ 37 | imSender.sendGroupMessage(recvIds,messageInfo); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/annotation/IMListener.java: -------------------------------------------------------------------------------- 1 | package com.im.client.annotation; 2 | 3 | import com.im.common.enums.IMListenerType; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Target({ElementType.TYPE,ElementType.FIELD}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Component 14 | public @interface IMListener { 15 | 16 | IMListenerType type(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.client.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.data.redis.connection.RedisConnectionFactory; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 13 | import org.springframework.data.redis.serializer.StringRedisSerializer; 14 | 15 | import javax.annotation.Resource; 16 | 17 | @Configuration("IMRedisConfig") 18 | public class RedisConfig { 19 | 20 | @Resource 21 | private RedisConnectionFactory factory; 22 | 23 | @Bean("IMRedisTemplate") 24 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 25 | RedisTemplate redisTemplate = new RedisTemplate(); 26 | redisTemplate.setConnectionFactory(redisConnectionFactory); 27 | // 设置值(value)的序列化采用jackson2JsonRedisSerializer 28 | redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); 29 | redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); 30 | // 设置键(key)的序列化采用StringRedisSerializer。 31 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 32 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 33 | redisTemplate.afterPropertiesSet(); 34 | return redisTemplate; 35 | } 36 | 37 | @Bean 38 | public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ 39 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 40 | ObjectMapper om = new ObjectMapper(); 41 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 42 | // 解决jackson2无法反序列化LocalDateTime的问题 43 | om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 44 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); 45 | jackson2JsonRedisSerializer.setObjectMapper(om); 46 | return jackson2JsonRedisSerializer; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/listener/MessageListener.java: -------------------------------------------------------------------------------- 1 | package com.im.client.listener; 2 | 3 | 4 | import com.im.common.model.SendResult; 5 | 6 | public interface MessageListener { 7 | 8 | void process(SendResult result); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/listener/MessageListenerMulticaster.java: -------------------------------------------------------------------------------- 1 | package com.im.client.listener; 2 | 3 | 4 | import com.im.client.annotation.IMListener; 5 | import com.im.common.enums.IMListenerType; 6 | import com.im.common.model.SendResult; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | @Component 14 | public class MessageListenerMulticaster { 15 | 16 | @Autowired(required = false) 17 | private List messageListeners = Collections.emptyList(); 18 | 19 | public void multicast(IMListenerType type, SendResult result){ 20 | for(MessageListener listener:messageListeners){ 21 | IMListener annotation = listener.getClass().getAnnotation(IMListener.class); 22 | if(annotation!=null && (annotation.type().equals(IMListenerType.ALL) || annotation.type().equals(type))){ 23 | listener.process(result); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/task/AbstractPullMessageTask.java: -------------------------------------------------------------------------------- 1 | package com.im.client.task; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.annotation.PreDestroy; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | 11 | @Slf4j 12 | public abstract class AbstractPullMessageTask { 13 | 14 | private int threadNum = 8; 15 | 16 | private ExecutorService executorService = Executors.newFixedThreadPool(threadNum); 17 | 18 | @PostConstruct 19 | public void init(){ 20 | // 初始化定时器 21 | for(int i=0;i redisTemplate; 20 | 21 | @Autowired 22 | private MessageListenerMulticaster listenerMulticaster; 23 | 24 | @Override 25 | public void pullMessage() { 26 | String key = RedisKey.IM_RESULT_GROUP_QUEUE; 27 | SendResult result = (SendResult)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS); 28 | if(result != null) { 29 | listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/im/client/task/PullSendResultPrivateMessageTask.java: -------------------------------------------------------------------------------- 1 | package com.im.client.task; 2 | 3 | import com.im.client.listener.MessageListenerMulticaster; 4 | import com.im.common.contant.RedisKey; 5 | import com.im.common.enums.IMListenerType; 6 | import com.im.common.model.SendResult; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.concurrent.TimeUnit; 14 | 15 | ; 16 | 17 | @Slf4j 18 | @Component 19 | public class PullSendResultPrivateMessageTask extends AbstractPullMessageTask{ 20 | 21 | 22 | @Qualifier("IMRedisTemplate") 23 | @Autowired 24 | private RedisTemplate redisTemplate; 25 | 26 | @Autowired 27 | private MessageListenerMulticaster listenerMulticaster; 28 | 29 | @Override 30 | public void pullMessage() { 31 | String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; 32 | SendResult result = (SendResult)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS); 33 | if(result != null) { 34 | listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /im-client/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.im.client.IMAutoConfiguration -------------------------------------------------------------------------------- /im-commom/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | im 7 | com.im 8 | 1.1.0 9 | 10 | 4.0.0 11 | im-commom 12 | 13 | 14 | 8 15 | 8 16 | 17 | 18 | 19 | 20 | org.projectlombok 21 | lombok 22 | ${lombok.version} 23 | 24 | 25 | cn.hutool 26 | hutool-all 27 | 28 | 29 | 30 | com.alibaba 31 | fastjson 32 | 33 | 34 | org.apache.commons 35 | commons-lang3 36 | 37 | 38 | 39 | org.springframework 40 | spring-beans 41 | 42 | 43 | org.apache.velocity 44 | velocity 45 | ${velocity.version} 46 | 47 | 48 | com.fasterxml.jackson.datatype 49 | jackson-datatype-joda 50 | 2.9.10 51 | 52 | 53 | org.springframework 54 | spring-context 55 | 56 | 57 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/contant/Constant.java: -------------------------------------------------------------------------------- 1 | package com.im.common.contant; 2 | 3 | 4 | public class Constant { 5 | 6 | // 在线状态过期时间 600s 7 | public static final long ONLINE_TIMEOUT_SECOND = 600; 8 | // 消息允许撤回时间 300s 9 | public static final long ALLOW_RECALL_SECOND = 300; 10 | } 11 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/contant/RedisKey.java: -------------------------------------------------------------------------------- 1 | package com.im.common.contant; 2 | 3 | public class RedisKey { 4 | 5 | // im-server最大id,从0开始递增 6 | public final static String IM_MAX_SERVER_ID = "im:max_server_id"; 7 | // 用户ID所连接的IM-server的ID 8 | public final static String IM_USER_SERVER_ID = "im:user:server_id:"; 9 | // 未读私聊消息队列 10 | public final static String IM_UNREAD_PRIVATE_QUEUE = "im:unread:private:"; 11 | // 未读群聊消息队列 12 | public final static String IM_UNREAD_GROUP_QUEUE = "im:unread:group:"; 13 | // 私聊消息发送结果队列 14 | public final static String IM_RESULT_PRIVATE_QUEUE = "im:result:private"; 15 | // 群聊消息发送结果队列 16 | public final static String IM_RESULT_GROUP_QUEUE = "im:result:group"; 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/enums/IMCmdType.java: -------------------------------------------------------------------------------- 1 | package com.im.common.enums; 2 | 3 | 4 | 5 | public enum IMCmdType { 6 | 7 | LOGIN(0,"登陆"), 8 | HEART_BEAT(1,"心跳"), 9 | FORCE_LOGUT(2,"强制下线"), 10 | PRIVATE_MESSAGE(3,"私聊消息"), 11 | GROUP_MESSAGE(4,"群发消息"); 12 | 13 | 14 | private Integer code; 15 | 16 | private String desc; 17 | 18 | IMCmdType(Integer index, String desc) { 19 | this.code =index; 20 | this.desc=desc; 21 | } 22 | 23 | public static IMCmdType fromCode(Integer code){ 24 | for (IMCmdType typeEnum:values()) { 25 | if (typeEnum.code.equals(code)) { 26 | return typeEnum; 27 | } 28 | } 29 | return null; 30 | } 31 | 32 | 33 | public String description() { 34 | return desc; 35 | } 36 | 37 | public Integer code(){ 38 | return this.code; 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/enums/IMListenerType.java: -------------------------------------------------------------------------------- 1 | package com.im.common.enums; 2 | 3 | public enum IMListenerType{ 4 | ALL(0,"全部消息"), 5 | PRIVATE_MESSAGE(1,"私聊消息"), 6 | GROUP_MESSAGE(2,"群聊消息"); 7 | 8 | private Integer code; 9 | 10 | private String desc; 11 | 12 | IMListenerType(Integer index, String desc) { 13 | this.code =index; 14 | this.desc=desc; 15 | } 16 | 17 | 18 | public String description() { 19 | return desc; 20 | } 21 | 22 | 23 | public Integer code(){ 24 | return this.code; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/enums/IMSendCode.java: -------------------------------------------------------------------------------- 1 | package com.im.common.enums; 2 | 3 | 4 | public enum IMSendCode { 5 | 6 | SUCCESS(0,"发送成功"), 7 | NOT_ONLINE(1,"对方当前不在线"), 8 | NOT_FIND_CHANNEL(2,"未找到对方的channel"), 9 | UNKONW_ERROR(9999,"未知异常"); 10 | 11 | private int code; 12 | private String desc; 13 | 14 | // 构造方法 15 | IMSendCode(int code, String desc) { 16 | this.code = code; 17 | this.desc = desc; 18 | } 19 | 20 | public String description() { 21 | return desc; 22 | } 23 | 24 | 25 | public Integer code(){ 26 | return this.code; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/GroupMessageInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import com.im.common.serializer.DateToLongSerializer; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import lombok.Data; 6 | 7 | import java.util.Date; 8 | 9 | @Data 10 | public class GroupMessageInfo { 11 | 12 | /* 13 | * 消息id 14 | */ 15 | private Long id; 16 | 17 | /* 18 | * 群聊id 19 | */ 20 | private Long groupId; 21 | 22 | /* 23 | * 发送者id 24 | */ 25 | private Long sendId; 26 | 27 | /* 28 | * 消息内容 29 | */ 30 | private String content; 31 | 32 | /* 33 | * 消息内容类型 具体枚举值由应用层定义 34 | */ 35 | private Integer type; 36 | 37 | /** 38 | * 发送时间 39 | */ 40 | @JsonSerialize(using = DateToLongSerializer.class) 41 | private Date sendTime; 42 | } 43 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/HeartbeatInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class HeartbeatInfo { 7 | 8 | private long userId; 9 | } 10 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/IMRecvInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class IMRecvInfo { 9 | 10 | /* 11 | * 命令类型 12 | */ 13 | private Integer cmd; 14 | 15 | /* 16 | * 接收者id列表 17 | */ 18 | private List recvIds; 19 | 20 | /* 21 | * 推送消息体 22 | */ 23 | private T data; 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/IMSendInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class IMSendInfo { 7 | 8 | /* 9 | * 命令 10 | */ 11 | private Integer cmd; 12 | 13 | /* 14 | * 推送消息体 15 | */ 16 | private T data; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/LoginInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class LoginInfo { 7 | 8 | private long userId; 9 | } 10 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/PrivateMessageInfo.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import com.im.common.serializer.DateToLongSerializer; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import lombok.Data; 6 | 7 | import java.util.Date; 8 | 9 | @Data 10 | public class PrivateMessageInfo { 11 | 12 | /* 13 | * 消息id 14 | */ 15 | private long id; 16 | 17 | /* 18 | * 发送者id 19 | */ 20 | private Long sendId; 21 | 22 | /* 23 | * 接收者id 24 | */ 25 | private Long recvId; 26 | 27 | /* 28 | * 发送内容 29 | */ 30 | private String content; 31 | 32 | /* 33 | * 消息内容类型 具体枚举值由应用层定义 34 | */ 35 | private Integer type; 36 | 37 | /** 38 | * 发送时间 39 | */ 40 | @JsonSerialize(using = DateToLongSerializer.class) 41 | private Date sendTime; 42 | } 43 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/model/SendResult.java: -------------------------------------------------------------------------------- 1 | package com.im.common.model; 2 | 3 | import com.im.common.enums.IMSendCode; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class SendResult { 8 | 9 | /* 10 | * 接收者id 11 | */ 12 | private Long recvId; 13 | 14 | /* 15 | * 发送状态 16 | */ 17 | private IMSendCode code; 18 | 19 | /* 20 | * 消息体(透传) 21 | */ 22 | private T messageInfo; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /im-commom/src/main/java/com/im/common/serializer/DateToLongSerializer.java: -------------------------------------------------------------------------------- 1 | package com.im.common.serializer; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonToken; 5 | import com.fasterxml.jackson.core.type.WritableTypeId; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | import com.fasterxml.jackson.databind.jsontype.TypeSerializer; 9 | 10 | import java.io.IOException; 11 | import java.util.Date; 12 | 13 | public class DateToLongSerializer extends JsonSerializer { 14 | 15 | @Override 16 | public void serialize(Date date, JsonGenerator jsonGenerator, 17 | SerializerProvider serializerProvider) throws IOException { 18 | jsonGenerator.writeNumber(date.getTime()); 19 | } 20 | 21 | @Override 22 | public void serializeWithType(Date value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException { 23 | WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen, 24 | typeSer.typeId(value, JsonToken.VALUE_STRING)); 25 | serialize(value, gen, serializers); 26 | typeSer.writeTypeSuffix(gen, typeIdDef); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/ImplatformApp.java: -------------------------------------------------------------------------------- 1 | package com.im.platform; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; 8 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 9 | 10 | 11 | @Slf4j 12 | @EnableAspectJAutoProxy(exposeProxy = true) 13 | @MapperScan(basePackages = {"com.bx.implatform.mapper"}) 14 | @SpringBootApplication(exclude= {SecurityAutoConfiguration.class })// 禁用secrity 15 | public class ImplatformApp { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(ImplatformApp.class,args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/GlobalCorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.Ordered; 7 | import org.springframework.web.cors.CorsConfiguration; 8 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 9 | import org.springframework.web.filter.CorsFilter; 10 | 11 | import java.util.Arrays; 12 | 13 | @Configuration 14 | public class GlobalCorsConfig { 15 | 16 | @Bean 17 | public FilterRegistrationBean corsFilter() { 18 | FilterRegistrationBean corsFilterFilterRegistrationBean = new FilterRegistrationBean<>(); 19 | //添加CORS配置信息 20 | CorsConfiguration corsConfiguration = new CorsConfiguration(); 21 | //允许的域,不要写*,否则cookie就无法使用了 22 | corsConfiguration.addAllowedOrigin("*"); 23 | //允许的头信息 24 | corsConfiguration.addAllowedHeader("*"); 25 | //允许的请求方式 26 | corsConfiguration.setAllowedMethods(Arrays.asList("POST", "PUT", "GET", "OPTIONS", "DELETE")); 27 | //是否发送cookie信息 28 | corsConfiguration.setAllowCredentials(true); 29 | //预检请求的有效期,单位为秒 30 | corsConfiguration.setMaxAge(3600L); 31 | 32 | //添加映射路径,标识待拦截的请求 33 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 34 | source.registerCorsConfiguration("/**", corsConfiguration); 35 | corsFilterFilterRegistrationBean.setFilter(new CorsFilter(source)); 36 | corsFilterFilterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); 37 | return corsFilterFilterRegistrationBean; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/ICEServer.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | 4 | import lombok.Data; 5 | 6 | 7 | @Data 8 | public class ICEServer { 9 | 10 | 11 | private String urls; 12 | 13 | 14 | private String username; 15 | 16 | 17 | private String credential; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/ICEServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Data 11 | @Component 12 | @ConfigurationProperties(prefix="webrtc") 13 | public class ICEServerConfig { 14 | 15 | private List iceServers = new ArrayList<>(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/MinIoClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | import io.minio.MinioClient; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | 9 | @Configuration 10 | public class MinIoClientConfig { 11 | @Value("${minio.endpoint}") 12 | private String endpoint; 13 | @Value("${minio.accessKey}") 14 | private String accessKey; 15 | @Value("${minio.secretKey}") 16 | private String secretKey; 17 | 18 | 19 | @Bean 20 | public MinioClient minioClient(){ 21 | // 注入minio 客户端 22 | MinioClient client = MinioClient.builder() 23 | .endpoint(endpoint) 24 | .credentials(accessKey, secretKey) 25 | .build(); 26 | return client; 27 | } 28 | } -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/MvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | import com.im.platform.interceptor.AuthInterceptor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | 12 | @Configuration 13 | public class MvcConfig implements WebMvcConfigurer { 14 | 15 | 16 | @Override 17 | public void addInterceptors(InterceptorRegistry registry) { 18 | registry.addInterceptor(authInterceptor()) 19 | .addPathPatterns("/**") 20 | .excludePathPatterns("/login","/logout","/register","/refreshToken", 21 | "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"); 22 | } 23 | 24 | 25 | @Bean 26 | public AuthInterceptor authInterceptor() { 27 | return new AuthInterceptor(); 28 | } 29 | 30 | @Bean 31 | public PasswordEncoder passwordEncoder(){ 32 | // 使用BCrypt加密密码 33 | return new BCryptPasswordEncoder(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.config; 2 | 3 | import io.swagger.annotations.ApiOperation; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.service.ApiInfo; 10 | import springfox.documentation.spi.DocumentationType; 11 | import springfox.documentation.spring.web.plugins.Docket; 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 13 | 14 | 15 | @Configuration 16 | @EnableSwagger2 17 | public class SwaggerConfig { 18 | 19 | @Bean 20 | public Docket createRestApi() { 21 | 22 | return new Docket(DocumentationType.SWAGGER_2) 23 | .apiInfo(apiInfo()) 24 | .select() 25 | //这里采用包含注解的方式来确定要显示的接口 26 | .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) 27 | //这里采用包扫描的方式来确定要显示的接口 28 | .paths(PathSelectors.any()) 29 | .build(); 30 | 31 | } 32 | 33 | private ApiInfo apiInfo() { 34 | return new ApiInfoBuilder() 35 | .title("IM Platform doc") 36 | .description("盒子IM API文档") 37 | .termsOfServiceUrl("http://8.134.92.70/") 38 | .version("1.0") 39 | .build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/contant/Constant.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.contant; 2 | 3 | 4 | public class Constant { 5 | // 最大图片上传大小 6 | public static final long MAX_IMAGE_SIZE = 5*1024*1024; 7 | // 最大上传文件大小 8 | public static final long MAX_FILE_SIZE = 10*1024*1024; 9 | // 群聊最大人数 10 | public static final long MAX_GROUP_MEMBER = 500; 11 | // accessToken 过期时间(1小时) 12 | public static final Integer ACCESS_TOKEN_EXPIRE = 30 * 60; 13 | // refreshToken 过期时间(7天) 14 | public static final Integer REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 ; 15 | // accessToken 加密秘钥 16 | public static final String ACCESS_TOKEN_SECRET = "MIIBIjANBgkq"; 17 | // refreshToken 加密秘钥 18 | public static final String REFRESH_TOKEN_SECRET = "IKDiqVmn0VFU"; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/contant/RedisKey.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.contant; 2 | 3 | public class RedisKey { 4 | 5 | // 已读群聊消息位置(已读最大id) 6 | public final static String IM_GROUP_READED_POSITION = "im:readed:group:position:"; 7 | // 缓存前缀 8 | public final static String IM_CACHE = "im:cache:"; 9 | // 缓存是否好友:bool 10 | public final static String IM_CACHE_FRIEND = IM_CACHE+"friend"; 11 | // 缓存群聊信息 12 | public final static String IM_CACHE_GROUP = IM_CACHE+"group"; 13 | // 缓存群聊成员id 14 | public final static String IM_CACHE_GROUP_MEMBER_ID = IM_CACHE+"group_member_ids"; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/FileController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | import com.im.platform.result.Result; 4 | import com.im.platform.result.ResultUtils; 5 | import com.im.platform.service.thirdparty.FileService; 6 | import com.im.platform.vo.UploadImageVO; 7 | import io.swagger.annotations.Api; 8 | import io.swagger.annotations.ApiOperation; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import org.springframework.web.multipart.MultipartFile; 14 | 15 | 16 | @Slf4j 17 | @RestController 18 | @Api(tags = "文件上传") 19 | public class FileController { 20 | 21 | 22 | @Autowired 23 | private FileService fileService; 24 | 25 | @ApiOperation(value = "上传图片",notes="上传图片,上传后返回原图和缩略图的url") 26 | @PostMapping("/image/upload") 27 | public Result uploadImage(MultipartFile file) { 28 | return ResultUtils.success(fileService.uploadImage(file)); 29 | } 30 | 31 | @ApiOperation(value = "上传文件",notes="上传文件,上传后返回文件url") 32 | @PostMapping("/file/upload") 33 | public Result uploadFile(MultipartFile file) { 34 | return ResultUtils.success(fileService.uploadFile(file),""); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/FriendController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | import com.im.platform.service.IFriendService; 4 | import com.im.platform.entity.Friend; 5 | import com.im.platform.result.Result; 6 | import com.im.platform.result.ResultUtils; 7 | import com.im.platform.session.SessionContext; 8 | import com.im.platform.vo.FriendVO; 9 | import io.swagger.annotations.Api; 10 | import io.swagger.annotations.ApiOperation; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.validation.Valid; 15 | import javax.validation.constraints.NotEmpty; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | @Api(tags = "好友") 20 | @RestController 21 | @RequestMapping("/friend") 22 | public class FriendController { 23 | 24 | @Autowired 25 | private IFriendService friendService; 26 | 27 | @GetMapping("/list") 28 | @ApiOperation(value = "好友列表",notes="获取好友列表") 29 | public Result< List> findFriends(){ 30 | List friends = friendService.findFriendByUserId(SessionContext.getSession().getId()); 31 | List vos = friends.stream().map(f->{ 32 | FriendVO vo = new FriendVO(); 33 | vo.setId(f.getFriendId()); 34 | vo.setHeadImage(f.getFriendHeadImage()); 35 | vo.setNickName(f.getFriendNickName()); 36 | return vo; 37 | }).collect(Collectors.toList()); 38 | return ResultUtils.success(vos); 39 | } 40 | 41 | 42 | 43 | @PostMapping("/add") 44 | @ApiOperation(value = "添加好友",notes="双方建立好友关系") 45 | public Result addFriend(@NotEmpty(message = "好友id不可为空") @RequestParam("friendId") Long friendId){ 46 | friendService.addFriend(friendId); 47 | return ResultUtils.success(); 48 | } 49 | 50 | @GetMapping("/find/{friendId}") 51 | @ApiOperation(value = "查找好友信息",notes="查找好友信息") 52 | public Result findFriend(@NotEmpty(message = "好友id不可为空") @PathVariable("friendId") Long friendId){ 53 | return ResultUtils.success(friendService.findFriend(friendId)); 54 | } 55 | 56 | 57 | @DeleteMapping("/delete/{friendId}") 58 | @ApiOperation(value = "删除好友",notes="解除好友关系") 59 | public Result delFriend(@NotEmpty(message = "好友id不可为空") @PathVariable("friendId") Long friendId){ 60 | friendService.delFriend(friendId); 61 | return ResultUtils.success(); 62 | } 63 | 64 | @PutMapping("/update") 65 | @ApiOperation(value = "更新好友信息",notes="更新好友头像或昵称") 66 | public Result modifyFriend(@Valid @RequestBody FriendVO vo){ 67 | friendService.update(vo); 68 | return ResultUtils.success(); 69 | } 70 | 71 | 72 | } 73 | 74 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/GroupController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | 4 | import com.im.platform.service.IGroupService; 5 | import com.im.platform.result.Result; 6 | import com.im.platform.result.ResultUtils; 7 | import com.im.platform.vo.GroupInviteVO; 8 | import com.im.platform.vo.GroupMemberVO; 9 | import com.im.platform.vo.GroupVO; 10 | import io.swagger.annotations.Api; 11 | import io.swagger.annotations.ApiOperation; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.validation.Valid; 16 | import javax.validation.constraints.NotEmpty; 17 | import javax.validation.constraints.NotNull; 18 | import java.util.List; 19 | 20 | 21 | @Api(tags = "群聊") 22 | @RestController 23 | @RequestMapping("/group") 24 | public class GroupController { 25 | 26 | @Autowired 27 | private IGroupService groupService; 28 | 29 | @ApiOperation(value = "创建群聊",notes="创建群聊") 30 | @PostMapping("/create") 31 | public Result createGroup(@NotEmpty(message = "群名不能为空") @RequestParam String groupName){ 32 | return ResultUtils.success(groupService.createGroup(groupName)); 33 | } 34 | 35 | @ApiOperation(value = "修改群聊信息",notes="修改群聊信息") 36 | @PutMapping("/modify") 37 | public Result modifyGroup(@Valid @RequestBody GroupVO vo){ 38 | return ResultUtils.success(groupService.modifyGroup(vo)); 39 | } 40 | 41 | @ApiOperation(value = "解散群聊",notes="解散群聊") 42 | @DeleteMapping("/delete/{groupId}") 43 | public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ 44 | groupService.deleteGroup(groupId); 45 | return ResultUtils.success(); 46 | } 47 | 48 | @ApiOperation(value = "查询群聊",notes="查询单个群聊信息") 49 | @GetMapping("/find/{groupId}") 50 | public Result findGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ 51 | return ResultUtils.success(groupService.findById(groupId)); 52 | } 53 | 54 | @ApiOperation(value = "查询群聊列表",notes="查询群聊列表") 55 | @GetMapping("/list") 56 | public Result> findGroups(){ 57 | return ResultUtils.success(groupService.findGroups()); 58 | } 59 | 60 | @ApiOperation(value = "邀请进群",notes="邀请好友进群") 61 | @PostMapping("/invite") 62 | public Result invite(@Valid @RequestBody GroupInviteVO vo){ 63 | groupService.invite(vo); 64 | return ResultUtils.success(); 65 | } 66 | 67 | @ApiOperation(value = "查询群聊成员",notes="查询群聊成员") 68 | @GetMapping("/members/{groupId}") 69 | public Result> findGroupMembers(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ 70 | return ResultUtils.success(groupService.findGroupMembers(groupId)); 71 | } 72 | 73 | @ApiOperation(value = "退出群聊",notes="退出群聊") 74 | @DeleteMapping("/quit/{groupId}") 75 | public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ 76 | groupService.quitGroup(groupId); 77 | return ResultUtils.success(); 78 | } 79 | 80 | @ApiOperation(value = "踢出群聊",notes="将用户踢出群聊") 81 | @DeleteMapping("/kick/{groupId}") 82 | public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, 83 | @NotNull(message = "用户id不能为空") @RequestParam Long userId){ 84 | groupService.kickGroup(groupId,userId); 85 | return ResultUtils.success(); 86 | } 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/GroupMessageController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | 4 | import com.im.common.model.GroupMessageInfo; 5 | import com.im.platform.service.IGroupMessageService; 6 | import com.im.platform.result.Result; 7 | import com.im.platform.result.ResultUtils; 8 | import com.im.platform.vo.GroupMessageVO; 9 | import io.swagger.annotations.Api; 10 | import io.swagger.annotations.ApiOperation; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.validation.Valid; 15 | import javax.validation.constraints.NotNull; 16 | import java.util.List; 17 | 18 | 19 | @Api(tags = "群聊消息") 20 | @RestController 21 | @RequestMapping("/message/group") 22 | public class GroupMessageController { 23 | 24 | @Autowired 25 | private IGroupMessageService groupMessageService; 26 | 27 | 28 | @PostMapping("/send") 29 | @ApiOperation(value = "发送群聊消息",notes="发送群聊消息") 30 | public Result sendMessage(@Valid @RequestBody GroupMessageVO vo){ 31 | return ResultUtils.success(groupMessageService.sendMessage(vo)); 32 | } 33 | 34 | @DeleteMapping("/recall/{id}") 35 | @ApiOperation(value = "撤回消息",notes="撤回群聊消息") 36 | public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){ 37 | groupMessageService.recallMessage(id); 38 | return ResultUtils.success(); 39 | } 40 | 41 | @PostMapping("/pullUnreadMessage") 42 | @ApiOperation(value = "拉取未读消息",notes="拉取未读消息") 43 | public Result pullUnreadMessage(){ 44 | groupMessageService.pullUnreadMessage(); 45 | return ResultUtils.success(); 46 | } 47 | 48 | @GetMapping("/history") 49 | @ApiOperation(value = "查询聊天记录",notes="查询聊天记录") 50 | public Result> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId, 51 | @NotNull(message = "页码不能为空") @RequestParam Long page, 52 | @NotNull(message = "size不能为空") @RequestParam Long size){ 53 | return ResultUtils.success( groupMessageService.findHistoryMessage(groupId,page,size)); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | 4 | import com.im.platform.service.IUserService; 5 | import com.im.platform.result.Result; 6 | import com.im.platform.result.ResultUtils; 7 | import com.im.platform.dto.LoginDTO; 8 | import com.im.platform.dto.RegisterDTO; 9 | import com.im.platform.vo.LoginVO; 10 | import io.swagger.annotations.Api; 11 | import io.swagger.annotations.ApiOperation; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.validation.Valid; 16 | 17 | 18 | 19 | @Api(tags = "用户登录和注册") 20 | @RestController 21 | public class LoginController { 22 | 23 | @Autowired 24 | private IUserService userService; 25 | 26 | @PostMapping("/login") 27 | @ApiOperation(value = "用户注册",notes="用户注册") 28 | public Result register(@Valid @RequestBody LoginDTO dto){ 29 | LoginVO vo = userService.login(dto); 30 | return ResultUtils.success(vo); 31 | } 32 | 33 | 34 | @PutMapping("/refreshToken") 35 | @ApiOperation(value = "刷新token",notes="用refreshtoken换取新的token") 36 | public Result refreshToken(@RequestHeader("refreshToken")String refreshToken){ 37 | LoginVO vo = userService.refreshToken(refreshToken); 38 | return ResultUtils.success(vo); 39 | } 40 | 41 | 42 | @PostMapping("/register") 43 | @ApiOperation(value = "用户注册",notes="用户注册") 44 | public Result register(@Valid @RequestBody RegisterDTO dto){ 45 | userService.register(dto); 46 | return ResultUtils.success(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/PrivateMessageController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | 4 | import com.im.common.model.PrivateMessageInfo; 5 | import com.im.platform.service.IPrivateMessageService; 6 | import com.im.platform.vo.PrivateMessageVO; 7 | import com.im.platform.result.Result; 8 | import com.im.platform.result.ResultUtils; 9 | import io.swagger.annotations.Api; 10 | import io.swagger.annotations.ApiOperation; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.validation.Valid; 15 | import javax.validation.constraints.NotNull; 16 | import java.util.List; 17 | 18 | @Api(tags = "私聊消息") 19 | @RestController 20 | @RequestMapping("/message/private") 21 | public class PrivateMessageController { 22 | 23 | @Autowired 24 | private IPrivateMessageService privateMessageService; 25 | 26 | @PostMapping("/send") 27 | @ApiOperation(value = "发送消息",notes="发送私聊消息") 28 | public Result sendMessage(@Valid @RequestBody PrivateMessageVO vo){ 29 | return ResultUtils.success(privateMessageService.sendMessage(vo)); 30 | } 31 | 32 | 33 | @DeleteMapping("/recall/{id}") 34 | @ApiOperation(value = "撤回消息",notes="撤回私聊消息") 35 | public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){ 36 | privateMessageService.recallMessage(id); 37 | return ResultUtils.success(); 38 | } 39 | 40 | 41 | @PostMapping("/pullUnreadMessage") 42 | @ApiOperation(value = "拉取未读消息",notes="拉取未读消息") 43 | public Result pullUnreadMessage(){ 44 | privateMessageService.pullUnreadMessage(); 45 | return ResultUtils.success(); 46 | } 47 | 48 | 49 | @GetMapping("/history") 50 | @ApiOperation(value = "查询聊天记录",notes="查询聊天记录") 51 | public Result> recallMessage(@NotNull(message = "好友id不能为空") @RequestParam Long friendId, 52 | @NotNull(message = "页码不能为空") @RequestParam Long page, 53 | @NotNull(message = "size不能为空") @RequestParam Long size){ 54 | return ResultUtils.success( privateMessageService.findHistoryMessage(friendId,page,size)); 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.controller; 2 | 3 | import com.im.platform.entity.User; 4 | import com.im.platform.service.IUserService; 5 | import com.im.platform.result.Result; 6 | import com.im.platform.result.ResultUtils; 7 | import com.im.platform.session.SessionContext; 8 | import com.im.platform.session.UserSession; 9 | import com.im.platform.util.BeanUtils; 10 | import com.im.platform.vo.UserVO; 11 | import io.swagger.annotations.Api; 12 | import io.swagger.annotations.ApiOperation; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.validation.Valid; 17 | import javax.validation.constraints.NotEmpty; 18 | import java.util.List; 19 | 20 | 21 | @Api(tags = "用户") 22 | @RestController 23 | @RequestMapping("/user") 24 | public class UserController { 25 | 26 | @Autowired 27 | private IUserService userService; 28 | 29 | 30 | @GetMapping("/online") 31 | @ApiOperation(value = "判断用户是否在线",notes="返回在线的用户id集合") 32 | public Result checkOnline(@NotEmpty @RequestParam("userIds") String userIds){ 33 | List onlineIds = userService.checkOnline(userIds); 34 | return ResultUtils.success(onlineIds); 35 | } 36 | 37 | @GetMapping("/self") 38 | @ApiOperation(value = "获取当前用户信息",notes="获取当前用户信息") 39 | public Result findSelfInfo(){ 40 | UserSession session = SessionContext.getSession(); 41 | User user = userService.getById(session.getId()); 42 | UserVO userVO = BeanUtils.copyProperties(user,UserVO.class); 43 | return ResultUtils.success(userVO); 44 | } 45 | 46 | 47 | @GetMapping("/find/{id}") 48 | @ApiOperation(value = "查找用户",notes="根据id查找用户") 49 | public Result findByIde(@NotEmpty @PathVariable("id") long id){ 50 | User user = userService.getById(id); 51 | UserVO userVO = BeanUtils.copyProperties(user,UserVO.class); 52 | return ResultUtils.success(userVO); 53 | } 54 | 55 | @PutMapping("/update") 56 | @ApiOperation(value = "修改用户信息",notes="修改用户信息,仅允许修改登录用户信息") 57 | public Result update(@Valid @RequestBody UserVO vo){ 58 | userService.update(vo); 59 | return ResultUtils.success(); 60 | } 61 | 62 | 63 | 64 | 65 | @GetMapping("/findByNickName") 66 | @ApiOperation(value = "查找用户",notes="根据昵称查找用户") 67 | public Result findByNickName(@NotEmpty(message = "用户昵称不可为空") @RequestParam("nickName") String nickName){ 68 | return ResultUtils.success( userService.findUserByNickName(nickName)); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/dto/LoginDTO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.dto; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | import javax.validation.constraints.NotEmpty; 9 | 10 | @Data 11 | @ApiModel("用户登录VO") 12 | public class LoginDTO { 13 | 14 | //@NotEmpty(message="用户名不可为空") 15 | @ApiModelProperty(value = "用户名") 16 | private String userName; 17 | 18 | // @NotEmpty(message="用户密码不可为空") 19 | @ApiModelProperty(value = "用户密码") 20 | private String password; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/dto/RegisterDTO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.dto; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | import javax.validation.constraints.NotEmpty; 9 | 10 | @Data 11 | @ApiModel("用户注册VO") 12 | public class RegisterDTO { 13 | 14 | @Length(max = 64,message = "用户名不能大于64字符") 15 | @NotEmpty(message="用户名不可为空") 16 | @ApiModelProperty(value = "用户名") 17 | private String userName; 18 | 19 | @Length(min=5,max = 20,message = "密码长度必须在5-20个字符之间") 20 | @NotEmpty(message="用户密码不可为空") 21 | @ApiModelProperty(value = "用户密码") 22 | private String password; 23 | 24 | @Length(max = 64,message = "昵称不能大于64字符") 25 | @NotEmpty(message="用户昵称不可为空") 26 | @ApiModelProperty(value = "用户昵称") 27 | private String nickName; 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/Friend.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | *

16 | * 好友 17 | *

18 | * 19 | * @author blue 20 | * @since 2022-10-22 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @TableName("im_friend") 25 | public class Friend extends Model { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * id 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 用户id 37 | */ 38 | @TableField("user_id") 39 | private Long userId; 40 | 41 | /** 42 | * 好友id 43 | */ 44 | @TableField("friend_id") 45 | private Long friendId; 46 | 47 | /** 48 | * 用户昵称 49 | */ 50 | @TableField("friend_nick_name") 51 | private String friendNickName; 52 | 53 | /** 54 | * 用户头像 55 | */ 56 | @TableField("friend_head_image") 57 | private String friendHeadImage; 58 | 59 | /** 60 | * 创建时间 61 | */ 62 | @TableField("created_time") 63 | private Date createdTime; 64 | 65 | 66 | @Override 67 | protected Serializable pkVal() { 68 | return this.id; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/Group.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | * 群 16 | * @author blue 17 | * @since 2022-10-31 18 | */ 19 | @Data 20 | @EqualsAndHashCode(callSuper = false) 21 | @TableName("im_group") 22 | public class Group extends Model { 23 | 24 | private static final long serialVersionUID = 1L; 25 | 26 | /** 27 | * id 28 | */ 29 | @TableId(value = "id", type = IdType.AUTO) 30 | private Long id; 31 | 32 | /** 33 | * 群名字 34 | */ 35 | @TableField("name") 36 | private String name; 37 | 38 | /** 39 | * 群主id 40 | */ 41 | @TableField("owner_id") 42 | private Long ownerId; 43 | 44 | /** 45 | * 头像 46 | */ 47 | @TableField("head_image") 48 | private String headImage; 49 | 50 | /** 51 | * 头像缩略图 52 | */ 53 | @TableField("head_image_thumb") 54 | private String headImageThumb; 55 | 56 | /** 57 | * 群公告 58 | */ 59 | @TableField("notice") 60 | private String notice; 61 | 62 | /** 63 | * 是否已删除 64 | */ 65 | @TableField("deleted") 66 | private Boolean deleted; 67 | 68 | /** 69 | * 创建时间 70 | */ 71 | @TableField("created_time") 72 | private Date createdTime; 73 | 74 | 75 | @Override 76 | protected Serializable pkVal() { 77 | return this.id; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/GroupMember.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | *

16 | * 群成员 17 | *

18 | * 19 | * @author blue 20 | * @since 2022-10-31 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @TableName("im_group_member") 25 | public class GroupMember extends Model { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * id 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 群id 37 | */ 38 | @TableField("group_id") 39 | private Long groupId; 40 | 41 | /** 42 | * 用户id 43 | */ 44 | @TableField("user_id") 45 | private Long userId; 46 | 47 | /** 48 | * 群内显示名称 49 | */ 50 | @TableField("alias_name") 51 | private String aliasName; 52 | 53 | /** 54 | * 头像 55 | */ 56 | @TableField("head_image") 57 | private String headImage; 58 | 59 | 60 | 61 | /** 62 | * 备注 63 | */ 64 | @TableField("remark") 65 | private String remark; 66 | 67 | /** 68 | * 是否已离开群聊 69 | */ 70 | @TableField("quit") 71 | private Boolean quit; 72 | 73 | 74 | /** 75 | * 创建时间 76 | */ 77 | @TableField("created_time") 78 | private Date createdTime; 79 | 80 | 81 | @Override 82 | protected Serializable pkVal() { 83 | return this.id; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/GroupMessage.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | *

16 | * 群消息 17 | *

18 | * 19 | * @author blue 20 | * @since 2022-10-31 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @TableName("im_group_message") 25 | public class GroupMessage extends Model { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * id 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 群id 37 | */ 38 | @TableField("group_id") 39 | private Long groupId; 40 | 41 | /** 42 | * 发送用户id 43 | */ 44 | @TableField("send_id") 45 | private Long sendId; 46 | 47 | /** 48 | * 发送内容 49 | */ 50 | @TableField("content") 51 | private String content; 52 | 53 | /** 54 | * 消息类型 0:文字 1:图片 2:文件 55 | */ 56 | @TableField("type") 57 | private Integer type; 58 | 59 | /** 60 | * 状态 61 | */ 62 | @TableField("status") 63 | private Integer status; 64 | 65 | /** 66 | * 发送时间 67 | */ 68 | @TableField("send_time") 69 | private Date sendTime; 70 | 71 | 72 | @Override 73 | protected Serializable pkVal() { 74 | return this.id; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/PrivateMessage.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | *

16 | * 17 | *

18 | * 19 | * @author blue 20 | * @since 2022-10-01 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @TableName("im_private_message") 25 | public class PrivateMessage extends Model { 26 | 27 | private static final long serialVersionUID=1L; 28 | 29 | /** 30 | * id 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 发送用户id 37 | */ 38 | @TableField("send_id") 39 | private Long sendId; 40 | 41 | /** 42 | * 接收用户id 43 | */ 44 | @TableField("recv_id") 45 | private Long recvId; 46 | 47 | /** 48 | * 发送内容 49 | */ 50 | @TableField("content") 51 | private String content; 52 | 53 | /** 54 | * 消息类型 0:文字 1:图片 2:文件 3:语音 10:撤回消息 55 | */ 56 | @TableField("type") 57 | private Integer type; 58 | 59 | /** 60 | * 状态 61 | */ 62 | @TableField("status") 63 | private Integer status; 64 | 65 | 66 | /** 67 | * 发送时间 68 | */ 69 | @TableField("send_time") 70 | private Date sendTime; 71 | 72 | 73 | @Override 74 | protected Serializable pkVal() { 75 | return this.id; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import java.io.Serializable; 12 | import java.util.Date; 13 | 14 | /** 15 | *

16 | * 用户 17 | *

18 | * 19 | * @author blue 20 | * @since 2022-10-01 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @TableName("im_user") 25 | public class User extends Model { 26 | 27 | private static final long serialVersionUID=1L; 28 | 29 | /** 30 | * id 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 用户名 37 | */ 38 | @TableField("user_name") 39 | private String userName; 40 | 41 | /** 42 | * 用户名 43 | */ 44 | @TableField("nick_name") 45 | private String nickName; 46 | 47 | /** 48 | * 性别 49 | */ 50 | @TableField("sex") 51 | private Integer sex; 52 | 53 | /** 54 | * 头像 55 | */ 56 | @TableField("head_image") 57 | private String headImage; 58 | 59 | /** 60 | * 头像缩略图 61 | */ 62 | @TableField("head_image_thumb") 63 | private String headImageThumb; 64 | 65 | 66 | /** 67 | * 个性签名 68 | */ 69 | @TableField("signature") 70 | private String signature; 71 | /** 72 | * 密码(明文) 73 | */ 74 | @TableField("password") 75 | private String password; 76 | 77 | /** 78 | * 最后登录时间 79 | */ 80 | @TableField("last_login_time") 81 | private Date lastLoginTime; 82 | 83 | /** 84 | * 创建时间 85 | */ 86 | @TableField("created_time") 87 | private Date createdTime; 88 | 89 | 90 | @Override 91 | protected Serializable pkVal() { 92 | return this.id; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/enums/FileType.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.enums; 2 | 3 | public enum FileType { 4 | 5 | FILE(0,"文件"), 6 | IMAGE(1,"图片"), 7 | VIDEO(2,"视频"), 8 | AUDIO(3,"声音"); 9 | 10 | 11 | 12 | private Integer code; 13 | 14 | private String desc; 15 | 16 | FileType(Integer index, String desc) { 17 | this.code =index; 18 | this.desc=desc; 19 | } 20 | 21 | public static FileType fromCode(Integer code){ 22 | for (FileType typeEnum:values()) { 23 | if (typeEnum.code.equals(code)) { 24 | return typeEnum; 25 | } 26 | } 27 | return null; 28 | } 29 | 30 | 31 | public String description() { 32 | return desc; 33 | } 34 | 35 | public Integer code(){ 36 | return this.code; 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/enums/MessageStatus.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.enums; 2 | 3 | 4 | public enum MessageStatus { 5 | 6 | UNREAD(0,"未读"), 7 | ALREADY_READ(1,"已读"), 8 | RECALL(2,"已撤回"); 9 | 10 | private Integer code; 11 | 12 | private String desc; 13 | 14 | MessageStatus(Integer index, String desc) { 15 | this.code =index; 16 | this.desc=desc; 17 | } 18 | 19 | public static MessageStatus fromCode(Integer code){ 20 | for (MessageStatus typeEnum:values()) { 21 | if (typeEnum.code.equals(code)) { 22 | return typeEnum; 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | 29 | public String description() { 30 | return desc; 31 | } 32 | 33 | public Integer code(){ 34 | return this.code; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/enums/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.enums; 2 | 3 | 4 | public enum MessageType { 5 | 6 | TEXT(0,"文字"), 7 | FILE(1,"文件"), 8 | IMAGE(2,"图片"), 9 | VIDEO(3,"视频"), 10 | TIP(10,"系统提示"), 11 | 12 | RTC_CALL(101,"呼叫"), 13 | RTC_ACCEPT(102,"接受"), 14 | RTC_REJECT(103, "拒绝"), 15 | RTC_CANCEL(104,"取消呼叫"), 16 | RTC_FAILED(105,"呼叫失败"), 17 | RTC_HANDUP(106,"挂断"), 18 | RTC_CANDIDATE(107,"同步candidate"); 19 | 20 | private Integer code; 21 | 22 | private String desc; 23 | 24 | MessageType(Integer index, String desc) { 25 | this.code =index; 26 | this.desc=desc; 27 | } 28 | 29 | 30 | public String description() { 31 | return desc; 32 | } 33 | 34 | public Integer code(){ 35 | return this.code; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/enums/ResultCode.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.enums; 2 | 3 | /** 4 | * 响应码枚举 5 | * 6 | * @author Blue 7 | * @date 2020/10/19 8 | * 9 | **/ 10 | public enum ResultCode { 11 | SUCCESS(200,"成功"), 12 | NO_LOGIN(400,"未登录"), 13 | INVALID_TOKEN(401,"token已失效"), 14 | PROGRAM_ERROR(500,"系统繁忙,请稍后再试"), 15 | PASSWOR_ERROR(10001,"密码不正确"), 16 | USERNAME_ALREADY_REGISTER(10003,"该用户名已注册"), 17 | ; 18 | 19 | 20 | private int code; 21 | private String msg; 22 | 23 | // 构造方法 24 | ResultCode(int code, String msg) { 25 | this.code = code; 26 | this.msg = msg; 27 | } 28 | public int getCode() { 29 | return code; 30 | } 31 | 32 | public void setCode(int code) { 33 | this.code = code; 34 | } 35 | 36 | public String getMsg() { 37 | return msg; 38 | } 39 | 40 | public void setMsg(String msg) { 41 | this.msg = msg; 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/exception/GlobalException.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.exception; 2 | 3 | import com.im.platform.enums.ResultCode; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | 9 | @Data 10 | public class GlobalException extends RuntimeException implements Serializable { 11 | private static final long serialVersionUID = 8134030011662574394L; 12 | private Integer code; 13 | private String message; 14 | 15 | public GlobalException(Integer code, String message){ 16 | this.code=code; 17 | this.message=message; 18 | } 19 | 20 | public GlobalException(ResultCode resultCode, String message){ 21 | this.code = resultCode.getCode(); 22 | this.message=message; 23 | } 24 | 25 | public GlobalException(ResultCode resultCode) { 26 | this.code = resultCode.getCode(); 27 | this.message = resultCode.getMsg(); 28 | } 29 | 30 | public GlobalException(String message){ 31 | this.code= ResultCode.PROGRAM_ERROR.getCode(); 32 | this.message=message; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/interceptor/AuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.interceptor; 2 | 3 | 4 | import com.alibaba.fastjson.JSON; 5 | import com.auth0.jwt.exceptions.JWTVerificationException; 6 | import com.im.platform.enums.ResultCode; 7 | import com.im.platform.contant.Constant; 8 | import com.im.platform.exception.GlobalException; 9 | import com.im.platform.session.UserSession; 10 | import com.im.platform.util.JwtUtil; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.web.method.HandlerMethod; 13 | import org.springframework.web.servlet.HandlerInterceptor; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | ; 17 | 18 | @Slf4j 19 | public class AuthInterceptor implements HandlerInterceptor { 20 | 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 24 | //如果不是映射到方法直接通过 25 | if (!(handler instanceof HandlerMethod)) { 26 | return true; 27 | } 28 | //从 http 请求头中取出 token 29 | String token = request.getHeader("accessToken"); 30 | if (token == null) { 31 | log.error("未登陆,url:{}",request.getRequestURI()); 32 | throw new GlobalException(ResultCode.NO_LOGIN); 33 | } 34 | try{ 35 | //验证 token 36 | JwtUtil.checkSign(token, Constant.ACCESS_TOKEN_SECRET); 37 | }catch ( 38 | JWTVerificationException e) { 39 | log.error("token已失效,url:{}",request.getRequestURI()); 40 | throw new GlobalException(ResultCode.INVALID_TOKEN); 41 | } 42 | // 存放session 43 | String strJson = JwtUtil.getInfo(token); 44 | UserSession userSession = JSON.parseObject(strJson,UserSession.class); 45 | request.setAttribute("session",userSession); 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/listener/GroupMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.listener; 2 | 3 | import com.im.client.annotation.IMListener; 4 | import com.im.client.listener.MessageListener; 5 | import com.im.common.enums.IMListenerType; 6 | import com.im.common.enums.IMSendCode; 7 | import com.im.common.model.GroupMessageInfo; 8 | import com.im.common.model.SendResult; 9 | import com.im.platform.contant.RedisKey; 10 | import com.im.platform.enums.MessageType; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.data.redis.core.RedisTemplate; 14 | 15 | 16 | @Slf4j 17 | @IMListener(type = IMListenerType.GROUP_MESSAGE) 18 | public class GroupMessageListener implements MessageListener { 19 | 20 | @Autowired 21 | private RedisTemplate redisTemplate; 22 | 23 | @Override 24 | public void process(SendResult result){ 25 | GroupMessageInfo messageInfo = (GroupMessageInfo) result.getMessageInfo(); 26 | if(messageInfo.getType().equals(MessageType.TIP)){ 27 | // 提示类数据不记录 28 | return; 29 | } 30 | 31 | // 保存该用户已拉取的最大消息id 32 | if(result.getCode().equals(IMSendCode.SUCCESS)) { 33 | String key = RedisKey.IM_GROUP_READED_POSITION + messageInfo.getGroupId() + ":" + result.getRecvId(); 34 | redisTemplate.opsForValue().set(key, messageInfo.getId()); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/listener/PrivateMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 4 | import com.im.client.IMClient; 5 | import com.im.client.annotation.IMListener; 6 | import com.im.client.listener.MessageListener; 7 | import com.im.common.enums.IMListenerType; 8 | import com.im.common.enums.IMSendCode; 9 | import com.im.common.model.PrivateMessageInfo; 10 | import com.im.common.model.SendResult; 11 | import com.im.platform.enums.MessageStatus; 12 | import com.im.platform.enums.MessageType; 13 | import com.im.platform.service.IPrivateMessageService; 14 | import com.im.platform.entity.PrivateMessage; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | 18 | import java.util.Date; 19 | 20 | 21 | @Slf4j 22 | @IMListener(type = IMListenerType.PRIVATE_MESSAGE) 23 | public class PrivateMessageListener implements MessageListener { 24 | 25 | @Autowired 26 | private IPrivateMessageService privateMessageService; 27 | 28 | @Autowired 29 | private IMClient imClient; 30 | 31 | @Override 32 | public void process(SendResult result){ 33 | PrivateMessageInfo messageInfo = (PrivateMessageInfo) result.getMessageInfo(); 34 | // 提示类数据不记录 35 | if(messageInfo.getType().equals(MessageType.TIP.code())){ 36 | 37 | return; 38 | } 39 | // 视频通话信令不记录 40 | if(messageInfo.getType() >= MessageType.RTC_CALL.code() && messageInfo.getType()< MessageType.RTC_CANDIDATE.code()){ 41 | // 通知用户呼叫失败了 42 | if(messageInfo.getType() == MessageType.RTC_CALL.code() 43 | && !result.getCode().equals(IMSendCode.SUCCESS)){ 44 | PrivateMessageInfo sendMessage = new PrivateMessageInfo(); 45 | sendMessage.setRecvId(messageInfo.getSendId()); 46 | sendMessage.setSendId(messageInfo.getRecvId()); 47 | sendMessage.setType(MessageType.RTC_FAILED.code()); 48 | sendMessage.setContent(result.getCode().description()); 49 | sendMessage.setSendTime(new Date()); 50 | imClient.sendPrivateMessage(sendMessage.getRecvId(),sendMessage); 51 | } 52 | } 53 | // 更新消息状态 54 | if(result.getCode().equals(IMSendCode.SUCCESS)){ 55 | UpdateWrapper updateWrapper = new UpdateWrapper<>(); 56 | updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId()) 57 | .eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code()) 58 | .set(PrivateMessage::getStatus, MessageStatus.ALREADY_READ.code()); 59 | privateMessageService.update(updateWrapper); 60 | log.info("消息已读,消息id:{},发送者:{},接收者:{}",messageInfo.getId(),messageInfo.getSendId(),messageInfo.getRecvId()); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/FriendMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.Friend; 5 | 6 | 7 | public interface FriendMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/GroupMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.Group; 5 | 6 | 7 | public interface GroupMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/GroupMemberMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.GroupMember; 5 | 6 | 7 | public interface GroupMemberMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/GroupMessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.GroupMessage; 5 | 6 | 7 | public interface GroupMessageMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/PrivateMessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.PrivateMessage; 5 | 6 | 7 | public interface PrivateMessageMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.im.platform.entity.User; 5 | 6 | 7 | public interface UserMapper extends BaseMapper { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/result/Result.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.result; 2 | 3 | import lombok.Data; 4 | 5 | 6 | @Data 7 | public class Result { 8 | 9 | 10 | private int code; 11 | 12 | private String message; 13 | 14 | private T data; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/result/ResultUtils.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.result; 2 | 3 | 4 | import com.im.platform.enums.ResultCode; 5 | 6 | public class ResultUtils { 7 | 8 | public static final Result success(){ 9 | Result result=new Result(); 10 | result.setCode(ResultCode.SUCCESS.getCode()); 11 | result.setMessage(ResultCode.SUCCESS.getMsg()); 12 | return result; 13 | } 14 | 15 | public static final Result success(T data){ 16 | Result result=new Result(); 17 | result.setCode(ResultCode.SUCCESS.getCode()); 18 | result.setMessage(ResultCode.SUCCESS.getMsg()); 19 | result.setData(data); 20 | return result; 21 | } 22 | 23 | public static final Result success(T data, String messsage){ 24 | Result result=new Result(); 25 | result.setCode(ResultCode.SUCCESS.getCode()); 26 | result.setMessage(messsage); 27 | result.setData(data); 28 | return result; 29 | } 30 | 31 | public static final Result success(String messsage){ 32 | Result result=new Result(); 33 | result.setCode(ResultCode.SUCCESS.getCode()); 34 | result.setMessage(messsage); 35 | return result; 36 | } 37 | 38 | public static final Result error(Integer code, String messsage){ 39 | Result result=new Result(); 40 | result.setCode(code); 41 | result.setMessage(messsage); 42 | return result; 43 | } 44 | 45 | 46 | public static final Result error(ResultCode resultCode, String messsage){ 47 | Result result=new Result(); 48 | result.setCode(resultCode.getCode()); 49 | result.setMessage(messsage); 50 | return result; 51 | } 52 | 53 | public static final Result error(ResultCode resultCode, String messsage, T data){ 54 | Result result=new Result(); 55 | result.setCode(resultCode.getCode()); 56 | result.setMessage(messsage); 57 | result.setData(data); 58 | return result; 59 | } 60 | 61 | public static final Result error(ResultCode resultCode){ 62 | Result result=new Result(); 63 | result.setCode(resultCode.getCode()); 64 | result.setMessage(resultCode.getMsg()); 65 | return result; 66 | } 67 | 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IFriendService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.platform.entity.Friend; 5 | import com.im.platform.vo.FriendVO; 6 | 7 | import java.util.List; 8 | 9 | 10 | public interface IFriendService extends IService { 11 | 12 | Boolean isFriend(Long userId1, Long userId2); 13 | 14 | List findFriendByUserId(Long UserId); 15 | 16 | void addFriend(Long friendId); 17 | 18 | void delFriend(Long friendId); 19 | 20 | void update(FriendVO vo); 21 | 22 | FriendVO findFriend(Long friendId); 23 | } 24 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IGroupMemberService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.platform.entity.GroupMember; 5 | 6 | import java.util.List; 7 | 8 | 9 | public interface IGroupMemberService extends IService { 10 | 11 | 12 | 13 | GroupMember findByGroupAndUserId(Long groupId,Long userId); 14 | 15 | List findByUserId(Long userId); 16 | 17 | List findByGroupId(Long groupId); 18 | 19 | List findUserIdsByGroupId(Long groupId); 20 | 21 | boolean save(GroupMember member); 22 | 23 | boolean saveOrUpdateBatch(Long groupId,List members); 24 | 25 | void removeByGroupId(Long groupId); 26 | 27 | void removeByGroupAndUserId(Long groupId,Long userId); 28 | } 29 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IGroupMessageService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.common.model.GroupMessageInfo; 5 | import com.im.platform.entity.GroupMessage; 6 | import com.im.platform.vo.GroupMessageVO; 7 | 8 | import java.util.List; 9 | 10 | 11 | public interface IGroupMessageService extends IService { 12 | 13 | 14 | Long sendMessage(GroupMessageVO vo); 15 | 16 | void recallMessage(Long id); 17 | 18 | void pullUnreadMessage(); 19 | 20 | List findHistoryMessage(Long groupId, Long page, Long size); 21 | } 22 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IGroupService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.platform.entity.Group; 5 | import com.im.platform.vo.GroupInviteVO; 6 | import com.im.platform.vo.GroupMemberVO; 7 | import com.im.platform.vo.GroupVO; 8 | 9 | import java.util.List; 10 | 11 | 12 | public interface IGroupService extends IService { 13 | 14 | 15 | GroupVO createGroup(String groupName); 16 | 17 | GroupVO modifyGroup(GroupVO vo); 18 | 19 | void deleteGroup(Long groupId); 20 | 21 | void quitGroup(Long groupId); 22 | 23 | void kickGroup(Long groupId,Long userId); 24 | 25 | List findGroups(); 26 | 27 | void invite(GroupInviteVO vo); 28 | 29 | Group GetById(Long groupId); 30 | 31 | GroupVO findById(Long groupId); 32 | 33 | List findGroupMembers(Long groupId); 34 | } 35 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IPrivateMessageService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.common.model.PrivateMessageInfo; 5 | import com.im.platform.vo.PrivateMessageVO; 6 | import com.im.platform.entity.PrivateMessage; 7 | 8 | import java.util.List; 9 | 10 | 11 | public interface IPrivateMessageService extends IService { 12 | 13 | Long sendMessage(PrivateMessageVO vo); 14 | 15 | void recallMessage(Long id); 16 | 17 | List findHistoryMessage(Long friendId, Long page,Long size); 18 | 19 | void pullUnreadMessage(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/service/IUserService.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.im.platform.entity.User; 5 | import com.im.platform.dto.LoginDTO; 6 | import com.im.platform.dto.RegisterDTO; 7 | import com.im.platform.vo.LoginVO; 8 | import com.im.platform.vo.UserVO; 9 | 10 | import java.util.List; 11 | 12 | 13 | public interface IUserService extends IService { 14 | 15 | LoginVO login(LoginDTO dto); 16 | 17 | LoginVO refreshToken(String refreshToken); 18 | 19 | void register(RegisterDTO dto); 20 | 21 | User findUserByName(String username); 22 | 23 | void update(UserVO vo); 24 | 25 | List findUserByNickName(String nickname); 26 | 27 | List checkOnline(String userIds); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/session/SessionContext.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.session; 2 | 3 | import org.springframework.web.context.request.RequestContextHolder; 4 | import org.springframework.web.context.request.ServletRequestAttributes; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | /* 9 | * @Description 10 | * @Author Blue 11 | * @Date 2022/10/21 12 | */ 13 | public class SessionContext { 14 | 15 | 16 | public static UserSession getSession(){ 17 | // 从请求上下文里获取Request对象 18 | ServletRequestAttributes requestAttributes = ServletRequestAttributes.class. 19 | cast(RequestContextHolder.getRequestAttributes()); 20 | HttpServletRequest request = requestAttributes.getRequest(); 21 | UserSession userSession = (UserSession) request.getAttribute("session"); 22 | return userSession; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/session/UserSession.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.session; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserSession { 7 | 8 | private Long id; 9 | private String userName; 10 | private String nickName; 11 | } 12 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.util; 2 | 3 | public class FileUtil { 4 | 5 | /** 6 | * 获取文件后缀 7 | * 8 | * @param fileName 文件名 9 | * @return boolean 10 | */ 11 | public static String getFileExtension(String fileName) { 12 | String extension = fileName.substring(fileName.lastIndexOf(".") + 1); 13 | return extension; 14 | } 15 | 16 | /** 17 | * 判断文件是否图片类型 18 | * 19 | * @param fileName 文件名 20 | * @return boolean 21 | */ 22 | public static boolean isImage(String fileName) { 23 | String extension = getFileExtension(fileName); 24 | String[] imageExtension = new String[]{"jpeg", "jpg", "bmp", "png","webp","gif"}; 25 | for (String e : imageExtension){ 26 | if (extension.toLowerCase().equals(e)) { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/util/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.util; 2 | 3 | 4 | import lombok.extern.slf4j.Slf4j; 5 | import net.coobird.thumbnailator.Thumbnails; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.ByteArrayOutputStream; 9 | 10 | @Slf4j 11 | public class ImageUtil { 12 | 13 | //以下是常量,按照阿里代码开发规范,不允许代码中出现魔法值 14 | private static final Integer ZERO = 0; 15 | private static final Integer ONE_ZERO_TWO_FOUR = 1024; 16 | private static final Integer NINE_ZERO_ZERO = 900; 17 | private static final Integer THREE_TWO_SEVEN_FIVE = 3275; 18 | private static final Integer TWO_ZERO_FOUR_SEVEN = 2047; 19 | private static final Double ZERO_EIGHT_FIVE = 0.85; 20 | private static final Double ZERO_SIX = 0.6; 21 | private static final Double ZERO_FOUR_FOUR = 0.44; 22 | private static final Double ZERO_FOUR = 0.4; 23 | 24 | /** 25 | * 根据指定大小压缩图片 26 | * 27 | * @param imageBytes 源图片字节数组 28 | * @param desFileSize 指定图片大小,单位kb 29 | * @return 压缩质量后的图片字节数组 30 | */ 31 | public static byte[] compressForScale(byte[] imageBytes, long desFileSize) { 32 | if (imageBytes == null || imageBytes.length <= ZERO || imageBytes.length < desFileSize * ONE_ZERO_TWO_FOUR) { 33 | return imageBytes; 34 | } 35 | long srcSize = imageBytes.length; 36 | double accuracy = getAccuracy(srcSize / ONE_ZERO_TWO_FOUR); 37 | try { 38 | while (imageBytes.length > desFileSize * ONE_ZERO_TWO_FOUR) { 39 | ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes); 40 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(imageBytes.length); 41 | Thumbnails.of(inputStream) 42 | .scale(accuracy) 43 | .outputQuality(accuracy) 44 | .toOutputStream(outputStream); 45 | imageBytes = outputStream.toByteArray(); 46 | } 47 | log.info("图片原大小={}kb | 压缩后大小={}kb", 48 | srcSize / ONE_ZERO_TWO_FOUR, imageBytes.length / ONE_ZERO_TWO_FOUR); 49 | } catch (Exception e) { 50 | log.error("【图片压缩】msg=图片压缩失败!", e); 51 | } 52 | return imageBytes; 53 | } 54 | 55 | 56 | 57 | 58 | /** 59 | * 自动调节精度(经验数值) 60 | * 61 | * @param size 源图片大小 62 | * @return 图片压缩质量比 63 | */ 64 | private static double getAccuracy(long size) { 65 | double accuracy; 66 | if (size < NINE_ZERO_ZERO) { 67 | accuracy = ZERO_EIGHT_FIVE; 68 | } else if (size < TWO_ZERO_FOUR_SEVEN) { 69 | accuracy = ZERO_SIX; 70 | } else if (size < THREE_TWO_SEVEN_FIVE) { 71 | accuracy = ZERO_FOUR_FOUR; 72 | } else { 73 | accuracy = ZERO_FOUR; 74 | } 75 | return accuracy; 76 | } 77 | 78 | } 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/util/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.util; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTDecodeException; 7 | import java.util.Date; 8 | 9 | 10 | public class JwtUtil { 11 | 12 | /** 13 | * 生成jwt字符串,30分钟后过期 JWT(json web token) 14 | * @param userId 15 | * @param info 16 | * @param expireIn 17 | * @param secret 18 | * @return 19 | * */ 20 | public static String sign(Long userId, String info,long expireIn,String secret) { 21 | try { 22 | Date date = new Date(System.currentTimeMillis() + expireIn*1000); 23 | Algorithm algorithm = Algorithm.HMAC256(secret); 24 | return JWT.create() 25 | //将userId保存到token里面 26 | .withAudience(userId.toString()) 27 | //存放自定义数据 28 | .withClaim("info", info) 29 | //五分钟后token过期 30 | .withExpiresAt(date) 31 | //token的密钥 32 | .sign(algorithm); 33 | } catch (Exception e) { 34 | e.printStackTrace(); 35 | return null; 36 | } 37 | } 38 | 39 | /** 40 | * 根据token获取userId 41 | * @param token 42 | * @return 43 | * */ 44 | public static Long getUserId(String token) { 45 | try { 46 | String userId = JWT.decode(token).getAudience().get(0); 47 | return Long.parseLong(userId); 48 | }catch (JWTDecodeException e) { 49 | return null; 50 | } 51 | } 52 | 53 | /** 54 | * 根据token获取自定义数据info 55 | * @param token 56 | * @return 57 | * */ 58 | public static String getInfo(String token) { 59 | try { 60 | return JWT.decode(token).getClaim("info").asString(); 61 | }catch (JWTDecodeException e) { 62 | return null; 63 | } 64 | } 65 | 66 | /** 67 | * 校验token 68 | * @param token 69 | * @param secret 70 | * @return 71 | * */ 72 | public static boolean checkSign(String token,String secret) { 73 | Algorithm algorithm = Algorithm.HMAC256(secret); 74 | JWTVerifier verifier = JWT.require(algorithm) 75 | //.withClaim("username, username) 76 | .build(); 77 | verifier.verify(token); 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/FriendVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | 8 | import javax.validation.constraints.NotNull; 9 | 10 | @Data 11 | @ApiModel("好友信息VO") 12 | public class FriendVO { 13 | 14 | @NotNull(message = "好友id不可为空") 15 | @ApiModelProperty(value = "好友id") 16 | private Long id; 17 | 18 | @NotNull(message = "好友昵称不可为空") 19 | @ApiModelProperty(value = "好友昵称") 20 | private String nickName; 21 | 22 | 23 | @ApiModelProperty(value = "好友头像") 24 | private String headImage; 25 | } 26 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/GroupInviteVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotEmpty; 8 | import javax.validation.constraints.NotNull; 9 | import java.util.List; 10 | 11 | @Data 12 | @ApiModel("邀请好友进群请求VO") 13 | public class GroupInviteVO { 14 | 15 | @NotNull(message = "群id不可为空") 16 | @ApiModelProperty(value = "群id") 17 | private Long groupId; 18 | 19 | @NotEmpty(message = "群id不可为空") 20 | @ApiModelProperty(value = "好友id列表不可为空") 21 | private List friendIds; 22 | } 23 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/GroupMemberVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | 8 | @Data 9 | @ApiModel("群成员信息VO") 10 | public class GroupMemberVO { 11 | 12 | @ApiModelProperty("用户id") 13 | private Long userId; 14 | 15 | @ApiModelProperty("群内显示名称") 16 | private String aliasName; 17 | 18 | @ApiModelProperty("头像") 19 | private String headImage; 20 | 21 | @ApiModelProperty("是否已退出") 22 | private Boolean quit; 23 | 24 | @ApiModelProperty("备注") 25 | private String remark; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/GroupMessageVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | import javax.validation.constraints.NotEmpty; 9 | import javax.validation.constraints.NotNull; 10 | 11 | @Data 12 | @ApiModel("群聊消息VO") 13 | public class GroupMessageVO { 14 | 15 | @NotNull(message="群聊id不可为空") 16 | @ApiModelProperty(value = "群聊id") 17 | private Long groupId; 18 | 19 | 20 | @Length(max=1024,message = "内容长度不得大于1024") 21 | @NotEmpty(message="发送内容不可为空") 22 | @ApiModelProperty(value = "发送内容") 23 | private String content; 24 | 25 | @NotNull(message="消息类型不可为空") 26 | @ApiModelProperty(value = "消息类型") 27 | private Integer type; 28 | } 29 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/GroupVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | import javax.validation.constraints.NotEmpty; 9 | import javax.validation.constraints.NotNull; 10 | 11 | @Data 12 | @ApiModel("群信息VO") 13 | public class GroupVO { 14 | 15 | @NotNull(message = "群id不可为空") 16 | @ApiModelProperty(value = "群id") 17 | private Long id; 18 | 19 | @Length(max=20,message = "群名称长度不能大于20") 20 | @NotEmpty(message = "群名称不可为空") 21 | @ApiModelProperty(value = "群名称") 22 | private String name; 23 | 24 | @NotNull(message = "群主id不可为空") 25 | @ApiModelProperty(value = "群主id") 26 | private Long ownerId; 27 | 28 | @ApiModelProperty(value = "头像") 29 | private String headImage; 30 | 31 | @ApiModelProperty(value = "头像缩略图") 32 | private String headImageThumb; 33 | 34 | @Length(max=1024,message = "群聊显示长度不能大于1024") 35 | @ApiModelProperty(value = "群公告") 36 | private String notice; 37 | 38 | @Length(max=20,message = "群聊显示长度不能大于20") 39 | @ApiModelProperty(value = "用户在群显示昵称") 40 | private String aliasName; 41 | 42 | @Length(max=20,message = "群聊显示长度不能大于20") 43 | @ApiModelProperty(value = "群聊显示备注") 44 | private String remark; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/LoginVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | @Data 8 | @ApiModel("用户登录VO") 9 | public class LoginVO { 10 | 11 | @ApiModelProperty(value = "每次请求都必须在header中携带accessToken") 12 | private String accessToken; 13 | 14 | @ApiModelProperty(value = "accessToken过期时间(秒)") 15 | private Integer accessTokenExpiresIn; 16 | 17 | @ApiModelProperty(value = "accessToken过期后,通过refreshToken换取新的token") 18 | private String refreshToken; 19 | 20 | @ApiModelProperty(value = "refreshToken过期时间(秒)") 21 | private Integer refreshTokenExpiresIn; 22 | } 23 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/PrivateMessageVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | import org.hibernate.validator.constraints.Length; 8 | 9 | import javax.validation.constraints.NotEmpty; 10 | import javax.validation.constraints.NotNull; 11 | 12 | @Data 13 | @ApiModel("私聊消息VO") 14 | public class PrivateMessageVO { 15 | 16 | 17 | @NotNull(message="接收用户id不可为空") 18 | @ApiModelProperty(value = "接收用户id") 19 | private Long recvId; 20 | 21 | 22 | @Length(max=1024,message = "内容长度不得大于1024") 23 | @NotEmpty(message="发送内容不可为空") 24 | @ApiModelProperty(value = "发送内容") 25 | private String content; 26 | 27 | @NotNull(message="消息类型不可为空") 28 | @ApiModelProperty(value = "消息类型") 29 | private Integer type; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/UploadImageVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | @Data 8 | @ApiModel("图片上传VO") 9 | public class UploadImageVO { 10 | 11 | @ApiModelProperty(value = "原图") 12 | private String originUrl; 13 | 14 | @ApiModelProperty(value = "缩略图") 15 | private String thumbUrl; 16 | } 17 | -------------------------------------------------------------------------------- /im-platform/src/main/java/com/im/platform/vo/UserVO.java: -------------------------------------------------------------------------------- 1 | package com.im.platform.vo; 2 | 3 | 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | import org.hibernate.validator.constraints.Length; 8 | 9 | import javax.validation.constraints.NotEmpty; 10 | import javax.validation.constraints.NotNull; 11 | 12 | @Data 13 | @ApiModel("用户信息VO") 14 | public class UserVO { 15 | 16 | @NotNull(message = "用户id不能为空") 17 | @ApiModelProperty(value = "id") 18 | private Long id; 19 | 20 | @NotEmpty(message = "用户名不能为空") 21 | @Length(max = 64,message = "用户名不能大于64字符") 22 | @ApiModelProperty(value = "用户名") 23 | private String userName; 24 | 25 | @NotEmpty(message = "用户昵称不能为空") 26 | @Length(max = 64,message = "昵称不能大于64字符") 27 | @ApiModelProperty(value = "用户昵称") 28 | private String nickName; 29 | 30 | @ApiModelProperty(value = "性别") 31 | private Integer sex; 32 | 33 | @Length(max = 64,message = "个性签名不能大于1024个字符") 34 | @ApiModelProperty(value = "个性签名") 35 | private String signature; 36 | 37 | @ApiModelProperty(value = "头像") 38 | private String headImage; 39 | 40 | @ApiModelProperty(value = "头像缩略图") 41 | private String headImageThumb; 42 | 43 | 44 | @ApiModelProperty(value = "是否在线") 45 | private Boolean online; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /im-platform/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #这是配置服务的端口 2 | server: 3 | port: 8888 4 | #配置项目的数据源 5 | spring: 6 | datasource: 7 | driver-class-name: com.mysql.jdbc.Driver 8 | url: jdbc:mysql://localhost:3306/box-im?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true 9 | username: root 10 | password: root 11 | 12 | redis: 13 | host: 127.0.0.1 14 | port: 6379 15 | database: 1 16 | 17 | servlet: 18 | multipart: 19 | max-file-size: 50MB 20 | max-request-size: 50MB 21 | 22 | mybatis-plus: 23 | configuration: 24 | # 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射 25 | map-underscore-to-camel-case: false 26 | #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 27 | # mapper 28 | mapper-locations: 29 | # *.xml的具体路径 30 | - classpath*:mapper/*.xml 31 | minio: 32 | endpoint: http://127.0.0.1:9001 #内网地址 33 | public: http://127.0.0.1:9001 #外网访问地址 34 | accessKey: admin 35 | secretKey: 12345678 36 | bucketName: box-im 37 | imagePath: image 38 | filePath: file 39 | 40 | webrtc: 41 | iceServers: 42 | - urls: stun:stun.l.google.com:19302 43 | 44 | -------------------------------------------------------------------------------- /im-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | im 7 | com.im 8 | 1.1.0 9 | 10 | 4.0.0 11 | 12 | im-server 13 | 14 | 15 | 16 | 17 | com.im 18 | im-commom 19 | 1.1.0 20 | 21 | 22 | org.springframework.boot 23 | spring-boot 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | io.netty 31 | netty-all 32 | 4.1.42.Final 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-redis 38 | 39 | 40 | 41 | 42 | ${project.artifactId} 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-maven-plugin 47 | 2.0.3.RELEASE 48 | 49 | 50 | 51 | repackage 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/IMServerApp.java: -------------------------------------------------------------------------------- 1 | package com.im.server; 2 | 3 | 4 | import org.springframework.boot.CommandLineRunner; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.scheduling.annotation.EnableAsync; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | 11 | 12 | @EnableAsync 13 | @EnableScheduling 14 | @ComponentScan(basePackages={"com.im"}) 15 | @SpringBootApplication 16 | public class IMServerApp implements CommandLineRunner { 17 | 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(IMServerApp.class,args); 21 | } 22 | 23 | 24 | @Override 25 | public void run(String... args) throws Exception { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.im.server.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.data.redis.connection.RedisConnectionFactory; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 14 | import org.springframework.data.redis.serializer.StringRedisSerializer; 15 | 16 | import javax.annotation.Resource; 17 | 18 | @Configuration 19 | public class RedisConfig { 20 | 21 | @Resource 22 | private RedisConnectionFactory factory; 23 | 24 | 25 | @Bean 26 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 27 | RedisTemplate redisTemplate = new RedisTemplate(); 28 | redisTemplate.setConnectionFactory(redisConnectionFactory); 29 | // 设置值(value)的序列化采用jackson2JsonRedisSerializer 30 | redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); 31 | redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); 32 | // 设置键(key)的序列化采用StringRedisSerializer。 33 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 34 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 35 | redisTemplate.afterPropertiesSet(); 36 | return redisTemplate; 37 | } 38 | 39 | @Bean 40 | public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ 41 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 42 | ObjectMapper om = new ObjectMapper(); 43 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 44 | // 解决jackson2无法反序列化LocalDateTime的问题 45 | om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 46 | om.registerModule(new JavaTimeModule()); 47 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); 48 | jackson2JsonRedisSerializer.setObjectMapper(om); 49 | return jackson2JsonRedisSerializer; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/IMChannelHandler.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty; 2 | 3 | import com.im.common.contant.RedisKey; 4 | import com.im.common.enums.IMCmdType; 5 | import com.im.common.model.IMSendInfo; 6 | import com.im.server.netty.processor.ProcessorFactory; 7 | import com.im.server.netty.processor.MessageProcessor; 8 | import com.im.server.util.SpringContextHolder; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.channel.SimpleChannelInboundHandler; 11 | import io.netty.handler.timeout.IdleState; 12 | import io.netty.handler.timeout.IdleStateEvent; 13 | import io.netty.util.AttributeKey; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | 17 | 18 | /** 19 | * WebSocket 长连接下 文本帧的处理器 20 | * 实现浏览器发送文本回写 21 | * 浏览器连接状态监控 22 | */ 23 | @Slf4j 24 | public class IMChannelHandler extends SimpleChannelInboundHandler { 25 | 26 | /** 27 | * 读取到消息后进行处理 28 | * 29 | * @param ctx 30 | * @param sendInfo 31 | * @throws Exception 32 | */ 33 | @Override 34 | protected void channelRead0(ChannelHandlerContext ctx, IMSendInfo sendInfo) throws Exception { 35 | // 创建处理器进行处理 36 | MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.fromCode(sendInfo.getCmd())); 37 | processor.process(ctx,processor.transForm(sendInfo.getData())); 38 | } 39 | 40 | /** 41 | * 出现异常的处理 打印报错日志 42 | * 43 | * @param ctx 44 | * @param cause 45 | * @throws Exception 46 | */ 47 | @Override 48 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 49 | log.error(cause.getMessage()); 50 | //关闭上下文 51 | //ctx.close(); 52 | } 53 | 54 | /** 55 | * 监控浏览器上线 56 | * 57 | * @param ctx 58 | * @throws Exception 59 | */ 60 | @Override 61 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 62 | log.info(ctx.channel().id().asLongText() + "连接"); 63 | } 64 | 65 | @Override 66 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 67 | AttributeKey attr = AttributeKey.valueOf("USER_ID"); 68 | Long userId = ctx.channel().attr(attr).get(); 69 | ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId); 70 | // 判断一下,避免异地登录导致的误删 71 | if(context != null && ctx.channel().id().equals(context.channel().id())){ 72 | // 移除channel 73 | UserChannelCtxMap.removeChannelCtx(userId); 74 | // 用户下线 75 | RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate"); 76 | String key = RedisKey.IM_USER_SERVER_ID + userId; 77 | redisTemplate.delete(key); 78 | log.info("断开连接,userId:{}",userId); 79 | } 80 | } 81 | 82 | @Override 83 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 84 | if (evt instanceof IdleStateEvent) { 85 | IdleState state = ((IdleStateEvent) evt).state(); 86 | if (state == IdleState.READER_IDLE) { 87 | // 在规定时间内没有收到客户端的上行数据, 主动断开连接 88 | AttributeKey attr = AttributeKey.valueOf("USER_ID"); 89 | Long userId = ctx.channel().attr(attr).get(); 90 | log.info("心跳超时,即将断开连接,用户id:{} ",userId); 91 | ctx.channel().close(); 92 | } 93 | } else { 94 | super.userEventTriggered(ctx, evt); 95 | } 96 | 97 | } 98 | } -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/IMServer.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty; 2 | 3 | public interface IMServer { 4 | 5 | /** 6 | * 服务是否准备就绪 7 | * @return 8 | */ 9 | boolean isReady(); 10 | 11 | /** 12 | * 服务开启 13 | */ 14 | void start(); 15 | 16 | /** 17 | * 服务停止 18 | */ 19 | void stop(); 20 | } 21 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/IMServerGroup.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty; 2 | 3 | import com.im.common.contant.RedisKey; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.PreDestroy; 11 | import java.util.List; 12 | 13 | @Slf4j 14 | @Component 15 | public class IMServerGroup implements CommandLineRunner { 16 | 17 | /** 18 | * im-server的id 19 | */ 20 | public static volatile long serverId = 0; 21 | 22 | @Autowired 23 | RedisTemplate redisTemplate; 24 | 25 | @Autowired 26 | private List imServers; 27 | 28 | /*** 29 | * 判断服务器是否就绪 30 | * 31 | * @return 32 | **/ 33 | public boolean isReady(){ 34 | for(IMServer imServer:imServers){ 35 | if(!imServer.isReady()){ 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | @Override 43 | public void run(String... args) throws Exception { 44 | // 初始化SERVER_ID 45 | String key = RedisKey.IM_MAX_SERVER_ID; 46 | serverId = redisTemplate.opsForValue().increment(key,1); 47 | // 启动服务 48 | for(IMServer imServer:imServers){ 49 | imServer.start(); 50 | } 51 | } 52 | 53 | @PreDestroy 54 | public void destroy(){ 55 | // 停止服务 56 | for(IMServer imServer:imServers){ 57 | imServer.stop(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/UserChannelCtxMap.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | 9 | public class UserChannelCtxMap { 10 | 11 | /* 12 | * 维护userId和ctx的关联关系,格式:Map 13 | */ 14 | private static Map channelMap = new ConcurrentHashMap(); 15 | 16 | public static void addChannelCtx(Long userId,ChannelHandlerContext ctx){ 17 | channelMap.put(userId,ctx); 18 | } 19 | 20 | public static void removeChannelCtx(Long userId){ 21 | if(userId != null){ 22 | channelMap.remove(userId); 23 | } 24 | } 25 | 26 | public static ChannelHandlerContext getChannelCtx(Long userId){ 27 | if(userId == null){ 28 | return null; 29 | } 30 | return channelMap.get(userId); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/processor/HeartbeatProcessor.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.processor; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import com.im.common.contant.Constant; 5 | import com.im.common.contant.RedisKey; 6 | import com.im.common.enums.IMCmdType; 7 | import com.im.common.model.HeartbeatInfo; 8 | import com.im.common.model.IMSendInfo; 9 | import com.im.server.netty.ws.WebSocketServer; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.util.AttributeKey; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.util.HashMap; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | @Slf4j 21 | @Component 22 | public class HeartbeatProcessor extends MessageProcessor { 23 | 24 | 25 | @Autowired 26 | private WebSocketServer WSServer; 27 | 28 | @Autowired 29 | RedisTemplate redisTemplate; 30 | 31 | @Override 32 | public void process(ChannelHandlerContext ctx, HeartbeatInfo beatInfo) { 33 | // 响应ws 34 | IMSendInfo sendInfo = new IMSendInfo(); 35 | sendInfo.setCmd(IMCmdType.HEART_BEAT.code()); 36 | ctx.channel().writeAndFlush(sendInfo); 37 | 38 | // 设置属性 39 | AttributeKey attr = AttributeKey.valueOf("HEARTBEAt_TIMES"); 40 | Long heartbeatTimes = ctx.channel().attr(attr).get(); 41 | ctx.channel().attr(attr).set(++heartbeatTimes); 42 | if(heartbeatTimes%10 == 0){ 43 | // 每心跳10次,用户在线状态续一次命 44 | attr = AttributeKey.valueOf("USER_ID"); 45 | Long userId = ctx.channel().attr(attr).get(); 46 | String key = RedisKey.IM_USER_SERVER_ID+userId; 47 | redisTemplate.expire(key, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); 48 | } 49 | } 50 | 51 | 52 | @Override 53 | public HeartbeatInfo transForm(Object o) { 54 | HashMap map = (HashMap)o; 55 | HeartbeatInfo heartbeatInfo = BeanUtil.fillBeanWithMap(map, new HeartbeatInfo(), false); 56 | return heartbeatInfo; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/processor/LoginProcessor.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.processor; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import com.im.common.contant.Constant; 5 | import com.im.common.contant.RedisKey; 6 | import com.im.common.enums.IMCmdType; 7 | import com.im.common.model.IMSendInfo; 8 | import com.im.common.model.LoginInfo; 9 | import com.im.server.netty.IMServerGroup; 10 | import com.im.server.netty.UserChannelCtxMap; 11 | import com.im.server.netty.ws.WebSocketServer; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.util.AttributeKey; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | import org.springframework.stereotype.Component; 18 | 19 | import java.util.HashMap; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @Slf4j 23 | @Component 24 | public class LoginProcessor extends MessageProcessor { 25 | 26 | 27 | @Autowired 28 | private WebSocketServer WSServer; 29 | 30 | @Autowired 31 | RedisTemplate redisTemplate; 32 | 33 | @Override 34 | synchronized public void process(ChannelHandlerContext ctx, LoginInfo loginInfo) { 35 | log.info("用户登录,userId:{}",loginInfo.getUserId()); 36 | ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(loginInfo.getUserId()); 37 | if(context != null){ 38 | // 不允许多地登录,强制下线 39 | IMSendInfo sendInfo = new IMSendInfo(); 40 | sendInfo.setCmd(IMCmdType.FORCE_LOGUT.code()); 41 | context.channel().writeAndFlush(sendInfo); 42 | } 43 | // 绑定用户和channel 44 | UserChannelCtxMap.addChannelCtx(loginInfo.getUserId(),ctx); 45 | // 设置用户id属性 46 | AttributeKey attr = AttributeKey.valueOf("USER_ID"); 47 | ctx.channel().attr(attr).set(loginInfo.getUserId()); 48 | // 心跳次数 49 | attr = AttributeKey.valueOf("HEARTBEAt_TIMES"); 50 | ctx.channel().attr(attr).set(0L); 51 | // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期 52 | String key = RedisKey.IM_USER_SERVER_ID+loginInfo.getUserId(); 53 | redisTemplate.opsForValue().set(key, IMServerGroup.serverId, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); 54 | // 响应ws 55 | IMSendInfo sendInfo = new IMSendInfo(); 56 | sendInfo.setCmd(IMCmdType.LOGIN.code()); 57 | ctx.channel().writeAndFlush(sendInfo); 58 | } 59 | 60 | 61 | @Override 62 | public LoginInfo transForm(Object o) { 63 | HashMap map = (HashMap)o; 64 | LoginInfo loginInfo = BeanUtil.fillBeanWithMap(map, new LoginInfo(), false); 65 | return loginInfo; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/processor/MessageProcessor.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.processor; 2 | 3 | 4 | import io.netty.channel.ChannelHandlerContext; 5 | 6 | public abstract class MessageProcessor { 7 | 8 | public void process(ChannelHandlerContext ctx,T data){} 9 | 10 | public void process(T data){} 11 | 12 | public T transForm(Object o){ 13 | return (T)o; 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/processor/PrivateMessageProcessor.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.processor; 2 | 3 | import com.im.common.contant.RedisKey; 4 | import com.im.common.enums.IMCmdType; 5 | import com.im.common.enums.IMSendCode; 6 | import com.im.common.model.IMRecvInfo; 7 | import com.im.common.model.IMSendInfo; 8 | import com.im.common.model.PrivateMessageInfo; 9 | import com.im.common.model.SendResult; 10 | import com.im.server.netty.UserChannelCtxMap; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Component; 16 | 17 | @Slf4j 18 | @Component 19 | public class PrivateMessageProcessor extends MessageProcessor> { 20 | 21 | @Autowired 22 | private RedisTemplate redisTemplate; 23 | 24 | @Override 25 | public void process(IMRecvInfo recvInfo) { 26 | PrivateMessageInfo messageInfo = recvInfo.getData(); 27 | Long recvId = recvInfo.getRecvIds().get(0); 28 | log.info("接收到消息,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent()); 29 | try{ 30 | ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(recvId); 31 | if(channelCtx != null ){ 32 | // 推送消息到用户 33 | IMSendInfo sendInfo = new IMSendInfo(); 34 | sendInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); 35 | sendInfo.setData(messageInfo); 36 | channelCtx.channel().writeAndFlush(sendInfo); 37 | // 消息发送成功确认 38 | String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; 39 | SendResult sendResult = new SendResult(); 40 | sendResult.setRecvId(recvId); 41 | sendResult.setCode(IMSendCode.SUCCESS); 42 | sendResult.setMessageInfo(messageInfo); 43 | redisTemplate.opsForList().rightPush(key,sendResult); 44 | }else{ 45 | // 消息推送失败确认 46 | String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; 47 | SendResult sendResult = new SendResult(); 48 | sendResult.setRecvId(recvId); 49 | sendResult.setCode(IMSendCode.NOT_FIND_CHANNEL); 50 | sendResult.setMessageInfo(messageInfo); 51 | redisTemplate.opsForList().rightPush(key,sendResult); 52 | log.error("未找到WS连接,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent()); 53 | } 54 | }catch (Exception e){ 55 | // 消息推送失败确认 56 | String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; 57 | SendResult sendResult = new SendResult(); 58 | sendResult.setRecvId(recvId); 59 | sendResult.setCode(IMSendCode.UNKONW_ERROR); 60 | sendResult.setMessageInfo(messageInfo); 61 | redisTemplate.opsForList().rightPush(key,sendResult); 62 | log.error("发送异常,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent(),e); 63 | } 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/processor/ProcessorFactory.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.processor; 2 | 3 | import com.im.common.enums.IMCmdType; 4 | import com.im.server.util.SpringContextHolder; 5 | 6 | public class ProcessorFactory { 7 | 8 | public static MessageProcessor createProcessor(IMCmdType cmd){ 9 | MessageProcessor processor = null; 10 | switch (cmd){ 11 | case LOGIN: 12 | processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class); 13 | break; 14 | case HEART_BEAT: 15 | processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class); 16 | break; 17 | case PRIVATE_MESSAGE: 18 | processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(PrivateMessageProcessor.class); 19 | break; 20 | case GROUP_MESSAGE: 21 | processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean(GroupMessageProcessor.class); 22 | break; 23 | default: 24 | break; 25 | } 26 | return processor; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/tcp/endecode/MessageProtocolDecoder.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.tcp.endecode; 2 | 3 | import com.im.common.model.IMSendInfo; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.handler.codec.ReplayingDecoder; 8 | import io.netty.util.CharsetUtil; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.util.List; 12 | 13 | @Slf4j 14 | public class MessageProtocolDecoder extends ReplayingDecoder { 15 | 16 | protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { 17 | if(byteBuf.readableBytes()< 4){ 18 | return; 19 | } 20 | // 获取到包的长度 21 | long length=byteBuf.readLong(); 22 | // 转成IMSendInfo 23 | ByteBuf contentBuf = byteBuf.readBytes((int)length); 24 | String content = contentBuf.toString(CharsetUtil.UTF_8); 25 | ObjectMapper objectMapper = new ObjectMapper(); 26 | IMSendInfo sendInfo = objectMapper.readValue(content, IMSendInfo.class); 27 | list.add(sendInfo); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/tcp/endecode/MessageProtocolEncoder.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.tcp.endecode; 2 | 3 | import com.im.common.model.IMSendInfo; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.handler.codec.MessageToByteEncoder; 8 | 9 | public class MessageProtocolEncoder extends MessageToByteEncoder { 10 | 11 | @Override 12 | protected void encode(ChannelHandlerContext channelHandlerContext, IMSendInfo sendInfo, ByteBuf byteBuf) throws Exception { 13 | ObjectMapper objectMapper = new ObjectMapper(); 14 | String content = objectMapper.writeValueAsString(sendInfo); 15 | byte[] bytes = content.getBytes("UTF-8"); 16 | // 写入长度 17 | byteBuf.writeLong(bytes.length); 18 | // 写入命令体 19 | byteBuf.writeBytes(bytes); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/ws/endecode/MessageProtocolDecoder.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.ws.endecode; 2 | 3 | import com.im.common.model.IMSendInfo; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.MessageToMessageDecoder; 7 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 8 | 9 | import java.util.List; 10 | 11 | public class MessageProtocolDecoder extends MessageToMessageDecoder { 12 | 13 | @Override 14 | protected void decode(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame, List list) throws Exception { 15 | ObjectMapper objectMapper = new ObjectMapper(); 16 | IMSendInfo sendInfo = objectMapper.readValue(textWebSocketFrame.text(), IMSendInfo.class); 17 | list.add(sendInfo); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/netty/ws/endecode/MessageProtocolEncoder.java: -------------------------------------------------------------------------------- 1 | package com.im.server.netty.ws.endecode; 2 | 3 | import com.im.common.model.IMSendInfo; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.MessageToMessageEncoder; 7 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 8 | 9 | import java.util.List; 10 | 11 | public class MessageProtocolEncoder extends MessageToMessageEncoder { 12 | 13 | @Override 14 | protected void encode(ChannelHandlerContext channelHandlerContext, IMSendInfo sendInfo, List list) throws Exception { 15 | ObjectMapper objectMapper = new ObjectMapper(); 16 | String text = objectMapper.writeValueAsString(sendInfo); 17 | 18 | TextWebSocketFrame frame = new TextWebSocketFrame(text); 19 | list.add(frame); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/task/AbstractPullMessageTask.java: -------------------------------------------------------------------------------- 1 | package com.im.server.task; 2 | 3 | import com.im.server.netty.IMServerGroup; 4 | import lombok.SneakyThrows; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import javax.annotation.PostConstruct; 9 | import javax.annotation.PreDestroy; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | 13 | @Slf4j 14 | public abstract class AbstractPullMessageTask{ 15 | 16 | private int threadNum = 1; 17 | private ExecutorService executorService; 18 | 19 | @Autowired 20 | private IMServerGroup serverGroup; 21 | 22 | public AbstractPullMessageTask(){ 23 | this.threadNum = 1; 24 | } 25 | 26 | public AbstractPullMessageTask(int threadNum){ 27 | this.threadNum = threadNum; 28 | } 29 | 30 | @PostConstruct 31 | public void init(){ 32 | // 初始化定时器 33 | executorService = Executors.newFixedThreadPool(threadNum); 34 | 35 | for(int i=0;i redisTemplate; 27 | 28 | 29 | 30 | @Override 31 | public void pullMessage() { 32 | // 从redis拉取未读消息 33 | String key = RedisKey.IM_UNREAD_GROUP_QUEUE + IMServerGroup.serverId; 34 | List messageInfos = redisTemplate.opsForList().range(key,0,-1); 35 | for(Object o: messageInfos){ 36 | redisTemplate.opsForList().leftPop(key); 37 | IMRecvInfo recvInfo = (IMRecvInfo)o; 38 | MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.GROUP_MESSAGE); 39 | processor.process(recvInfo); 40 | } 41 | } 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/task/PullUnreadPrivateMessageTask.java: -------------------------------------------------------------------------------- 1 | package com.im.server.task; 2 | 3 | 4 | import com.im.common.contant.RedisKey; 5 | import com.im.common.enums.IMCmdType; 6 | import com.im.common.model.IMRecvInfo; 7 | import com.im.common.model.PrivateMessageInfo; 8 | import com.im.server.netty.IMServerGroup; 9 | import com.im.server.netty.processor.MessageProcessor; 10 | import com.im.server.netty.processor.ProcessorFactory; 11 | import com.im.server.netty.ws.WebSocketServer; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.util.List; 18 | 19 | 20 | @Slf4j 21 | @Component 22 | public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask { 23 | 24 | @Autowired 25 | private WebSocketServer WSServer; 26 | 27 | @Autowired 28 | private RedisTemplate redisTemplate; 29 | 30 | @Override 31 | public void pullMessage() { 32 | // 从redis拉取未读消息 33 | String key = RedisKey.IM_UNREAD_PRIVATE_QUEUE + IMServerGroup.serverId; 34 | List messageInfos = redisTemplate.opsForList().range(key,0,-1); 35 | for(Object o: messageInfos){ 36 | redisTemplate.opsForList().leftPop(key); 37 | IMRecvInfo recvInfo = (IMRecvInfo)o; 38 | MessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.PRIVATE_MESSAGE); 39 | processor.process(recvInfo); 40 | 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/im/server/util/SpringContextHolder.java: -------------------------------------------------------------------------------- 1 | package com.im.server.util; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | 9 | @Component 10 | public class SpringContextHolder implements ApplicationContextAware { 11 | 12 | private static ApplicationContext applicationContext; 13 | 14 | @Override 15 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 16 | SpringContextHolder.applicationContext = applicationContext; 17 | } 18 | 19 | public static ApplicationContext getApplicationContext() { 20 | assertApplicationContext(); 21 | return applicationContext; 22 | } 23 | 24 | public static T getBean(String beanName) { 25 | assertApplicationContext(); 26 | return (T) applicationContext.getBean(beanName); 27 | } 28 | 29 | public static T getBean(Class requiredType) { 30 | assertApplicationContext(); 31 | return applicationContext.getBean(requiredType); 32 | } 33 | 34 | private static void assertApplicationContext() { 35 | if (SpringContextHolder.applicationContext == null) { 36 | throw new RuntimeException("applicaitonContext属性为null,请检查是否注入了SpringContextHolder!"); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /im-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8877 3 | 4 | spring: 5 | redis: 6 | host: 127.0.0.1 7 | port: 6379 8 | database: 1 9 | 10 | websocket: 11 | enable: true 12 | port: 8878 13 | 14 | tcpsocket: 15 | enable: false # 暂时不开启 16 | port: 8879 -------------------------------------------------------------------------------- /im-ui/.env.development: -------------------------------------------------------------------------------- 1 | 2 | ENV = 'development' 3 | # app名称 4 | VUE_APP_NAME = "盒子IM" 5 | // 接口请求地址 6 | VUE_APP_BASE_API = '/api' 7 | # ws地址 8 | VUE_APP_WS_URL = 'ws://localhost:8878/im' -------------------------------------------------------------------------------- /im-ui/.env.production: -------------------------------------------------------------------------------- 1 | ENV = 'production' 2 | 3 | # app名称 4 | VUE_APP_NAME = "盒子IM" 5 | # 接口地址 6 | VUE_APP_BASE_API = 'https://www.boxim.online/api' 7 | # ws地址 8 | VUE_APP_WS_URL = 'wss://www.boxim.online:81/im' -------------------------------------------------------------------------------- /im-ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # 26 | package-lock.json 27 | -------------------------------------------------------------------------------- /im-ui/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /im-ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /im-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.1.3", 12 | "core-js": "^3.6.5", 13 | "element-ui": "^2.15.10", 14 | "js-audio-recorder": "^1.0.7", 15 | "sass": "^1.47.0", 16 | "sass-loader": "^7.3.1", 17 | "vue": "^2.6.11", 18 | "vue-axios": "^3.5.0", 19 | "vue-router": "^3.3.3", 20 | "vuex": "^3.6.2", 21 | "vuex-persist": "^3.1.3" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "~4.5.12", 25 | "@vue/cli-plugin-eslint": "~4.5.12", 26 | "@vue/cli-service": "~4.5.12", 27 | "babel-eslint": "^10.1.0", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-vue": "^6.2.2", 30 | "vue-template-compiler": "^2.6.11" 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "env": { 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/essential", 39 | "eslint:recommended" 40 | ], 41 | "parserOptions": { 42 | "parser": "babel-eslint" 43 | }, 44 | "rules": { 45 | "no-mixed-spaces-and-tabs": 0, 46 | "generator-star-spacing": "off", 47 | "no-tabs": "off", 48 | "no-unused-vars": "off", 49 | "no-unused-labels": "off", 50 | "no-console": "off", 51 | "vue/no-unused-components": "off", 52 | "no-irregular-whitespace": "off", 53 | "no-debugger": "off", 54 | "no-useless-escape": "off" 55 | } 56 | }, 57 | "browserslist": [ 58 | "> 1%", 59 | "last 2 versions", 60 | "not dead" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /im-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/public/favicon.ico -------------------------------------------------------------------------------- /im-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 盒子IM 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /im-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /im-ui/src/api/element.js: -------------------------------------------------------------------------------- 1 | let fixTop = (e) => { 2 | var offset = e.offsetTop 3 | if (e.offsetParent != null) { 4 | offset += fixTop(e.offsetParent) 5 | } 6 | return offset 7 | } 8 | 9 | let fixLeft = (e) => { 10 | var offset = e.offsetLeft 11 | if (e.offsetParent != null) { 12 | offset += fixLeft(e.offsetParent) 13 | } 14 | return offset 15 | } 16 | 17 | let setTitleTip = (tip) => { 18 | let title = process.env.VUE_APP_NAME; 19 | if(tip){ 20 | title = `(${tip})${title}`; 21 | } 22 | document.title =title; 23 | 24 | } 25 | 26 | export default{ 27 | fixTop, 28 | fixLeft, 29 | setTitleTip 30 | } 31 | -------------------------------------------------------------------------------- /im-ui/src/api/emotion.js: -------------------------------------------------------------------------------- 1 | const emoTextList = ['微笑', '撇嘴', '色', '发呆', '得意', '流泪', '害羞', '闭嘴', '睡', '大哭', '尴尬', '发怒', '调皮', '呲牙', '惊讶', '难过', '酷', '冷汗', '抓狂', '吐', '偷笑', '可爱', '白眼', '傲慢', '饥饿', '困', '惊恐', '流汗', '憨笑', '大兵', '奋斗', '咒骂', '疑问', '嘘', '晕', '折磨', '衰', '骷髅', '敲打', '再见', '擦汗', '抠鼻', '鼓掌', '糗大了', '坏笑', '左哼哼', '右哼哼', '哈欠', '鄙视', '委屈', '快哭了', '阴险', '亲亲', '吓', '可怜', '菜刀', '西瓜', '啤酒', '篮球', '乒乓', '咖啡', '饭', '猪头', '玫瑰', '凋谢', '示爱', '爱心', '心碎', '蛋糕', '闪电', '炸弹', '刀', '足球', '瓢虫', '便便', '月亮', '太阳', '礼物', '拥抱', '强', '弱', '握手', '胜利', '抱拳', '勾引', '拳头', '差劲', '爱你', 'NO', 'OK', '爱情', '飞吻', '跳跳', '发抖', '怄火', '转圈', '磕头', '回头', '跳绳', '挥手', '激动', '街舞', '献吻', '左太极', '右太极']; 2 | 3 | 4 | let transform = (content) => { 5 | return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg); 6 | } 7 | 8 | // 将匹配结果替换表情图片 9 | let textToImg = (emoText) => { 10 | let word = emoText.replace(/\#|\;/gi, ''); 11 | let idx = emoTextList.indexOf(word); 12 | let url = require(`@/assets/emoji/${idx}.gif`); 13 | return `` 14 | } 15 | 16 | 17 | export default { 18 | emoTextList, 19 | transform, 20 | textToImg 21 | } 22 | -------------------------------------------------------------------------------- /im-ui/src/api/enums.js: -------------------------------------------------------------------------------- 1 | 2 | const MESSAGE_TYPE = { 3 | RTC_CALL: 101, 4 | RTC_ACCEPT: 102, 5 | RTC_REJECT: 103, 6 | RTC_CANCEL: 104, 7 | RTC_FAILED: 105, 8 | RTC_HANDUP: 106, 9 | RTC_CANDIDATE: 107 10 | } 11 | 12 | const USER_STATE = { 13 | OFFLINE: 0, 14 | FREE: 1, 15 | BUSY: 2 16 | } 17 | 18 | export { 19 | MESSAGE_TYPE, 20 | USER_STATE 21 | } 22 | -------------------------------------------------------------------------------- /im-ui/src/api/httpRequest.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import router from '@/router' 3 | import { 4 | Message 5 | } from 'element-ui' 6 | 7 | const http = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, 9 | timeout: 1000 * 30, 10 | withCredentials: true, 11 | headers: { 12 | 'Content-Type': 'application/json; charset=utf-8' 13 | } 14 | }) 15 | 16 | /** 17 | * 请求拦截 18 | */ 19 | http.interceptors.request.use(config => { 20 | let accessToken = sessionStorage.getItem("accessToken"); 21 | if (accessToken) { 22 | config.headers.accessToken = encodeURIComponent(accessToken); 23 | } 24 | return config 25 | }, error => { 26 | return Promise.reject(error) 27 | }) 28 | 29 | /** 30 | * 响应拦截 31 | */ 32 | http.interceptors.response.use(async response => { 33 | if (response.data.code == 200) { 34 | return response.data.data; 35 | } else if (response.data.code == 400) { 36 | router.replace("/login"); 37 | } else if (response.data.code == 401) { 38 | console.log("token失效,尝试重新获取") 39 | let refreshToken = sessionStorage.getItem("refreshToken"); 40 | if (!refreshToken) { 41 | router.replace("/login"); 42 | } 43 | // 发送请求, 进行刷新token操作, 获取新的token 44 | const data = await http({ 45 | method: 'put', 46 | url: '/refreshToken', 47 | headers: { 48 | refreshToken: refreshToken 49 | } 50 | }) 51 | // 保存token 52 | sessionStorage.setItem("accessToken", data.accessToken); 53 | sessionStorage.setItem("refreshToken", data.refreshToken); 54 | // 这里需要把headers清掉,否则请求时会报错,原因暂不详... 55 | response.config.headers=undefined; 56 | // 重新发送刚才的请求 57 | return http(response.config) 58 | } else { 59 | Message({ 60 | message: response.data.message, 61 | type: 'error', 62 | duration: 1500, 63 | customClass: 'element-error-message-zindex' 64 | }) 65 | return Promise.reject(response.data) 66 | } 67 | }, error => { 68 | switch (error.response.status) { 69 | case 400: 70 | Message({ 71 | message: error.response.data, 72 | type: 'error', 73 | duration: 1500, 74 | customClass: 'element-error-message-zindex' 75 | }) 76 | break 77 | case 401: 78 | router.replace("/login"); 79 | break 80 | case 405: 81 | Message({ 82 | message: 'http请求方式有误', 83 | type: 'error', 84 | duration: 1500, 85 | customClass: 'element-error-message-zindex' 86 | }) 87 | break 88 | case 404: 89 | case 500: 90 | Message({ 91 | message: '服务器出了点小差,请稍后再试', 92 | type: 'error', 93 | duration: 1500, 94 | customClass: 'element-error-message-zindex' 95 | }) 96 | break 97 | case 501: 98 | Message({ 99 | message: '服务器不支持当前请求所需要的某个功能', 100 | type: 'error', 101 | duration: 1500, 102 | customClass: 'element-error-message-zindex' 103 | }) 104 | break 105 | } 106 | 107 | return Promise.reject(error) 108 | }) 109 | 110 | 111 | export default http 112 | -------------------------------------------------------------------------------- /im-ui/src/assets/audio/call.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/audio/call.wav -------------------------------------------------------------------------------- /im-ui/src/assets/audio/tip.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/audio/tip.wav -------------------------------------------------------------------------------- /im-ui/src/assets/default_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/default_head.png -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/0.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/1.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/10.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/100.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/100.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/101.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/101.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/102.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/102.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/103.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/103.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/104.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/104.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/11.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/12.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/12.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/13.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/14.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/14.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/15.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/15.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/16.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/17.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/17.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/18.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/18.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/19.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/19.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/2.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/20.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/20.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/21.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/21.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/22.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/22.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/23.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/23.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/24.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/24.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/25.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/25.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/26.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/26.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/27.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/27.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/28.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/28.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/29.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/29.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/3.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/30.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/30.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/31.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/31.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/32.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/33.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/33.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/34.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/34.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/35.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/35.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/36.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/36.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/37.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/37.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/38.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/38.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/39.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/39.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/4.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/40.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/40.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/41.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/41.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/42.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/42.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/43.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/43.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/44.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/44.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/45.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/45.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/46.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/46.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/47.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/47.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/48.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/49.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/49.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/5.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/50.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/50.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/51.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/51.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/52.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/52.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/53.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/53.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/54.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/54.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/55.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/55.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/56.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/56.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/57.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/57.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/58.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/58.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/59.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/59.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/6.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/60.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/60.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/61.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/61.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/62.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/62.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/63.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/63.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/64.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/65.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/65.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/66.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/66.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/67.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/67.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/68.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/68.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/69.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/69.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/7.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/70.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/70.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/71.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/71.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/72.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/72.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/73.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/73.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/74.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/74.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/75.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/75.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/76.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/76.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/77.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/77.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/78.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/78.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/79.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/79.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/8.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/80.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/80.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/81.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/81.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/82.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/82.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/83.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/83.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/84.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/84.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/85.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/85.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/86.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/86.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/87.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/87.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/88.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/88.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/89.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/89.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/9.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/90.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/90.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/91.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/91.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/92.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/92.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/93.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/93.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/94.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/94.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/95.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/95.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/96.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/96.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/97.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/97.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/98.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/98.gif -------------------------------------------------------------------------------- /im-ui/src/assets/emoji/99.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/emoji/99.gif -------------------------------------------------------------------------------- /im-ui/src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3791506 */ 3 | src: url('iconfont.woff2?t=1669336625993') format('woff2'), 4 | url('iconfont.woff?t=1669336625993') format('woff'), 5 | url('iconfont.ttf?t=1669336625993') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-biaoqing:before { 17 | content: "\e60c"; 18 | } 19 | 20 | .icon-youyinpin:before { 21 | content: "\e649"; 22 | } 23 | 24 | .icon-audio:before { 25 | content: "\e800"; 26 | } 27 | 28 | .icon-group_fill:before { 29 | content: "\e7f4"; 30 | } 31 | 32 | .icon-yinpin:before { 33 | content: "\e68a"; 34 | } 35 | 36 | .icon-emoji:before { 37 | content: "\e6f6"; 38 | } 39 | 40 | .icon-voiceprint:before { 41 | content: "\e953"; 42 | } 43 | 44 | .icon-phone-reject:before { 45 | content: "\e605"; 46 | } 47 | 48 | .icon-phone-accept:before { 49 | content: "\e8be"; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /im-ui/src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /im-ui/src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /im-ui/src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /im-ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/im-ui/src/assets/logo.png -------------------------------------------------------------------------------- /im-ui/src/assets/style/global.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | html { 4 | height: 100%; 5 | overflow: hidden; 6 | 7 | } 8 | 9 | body { 10 | height: 100%; 11 | margin: 0; 12 | overflow: hidden; 13 | 14 | } 15 | 16 | section { 17 | height: 100%; 18 | } 19 | 20 | .el-dialog__body{ 21 | padding: 10px 15px !important; 22 | } 23 | 24 | ::-webkit-scrollbar { 25 | width: 6px; 26 | height: 1px; 27 | } 28 | 29 | ::-webkit-scrollbar-thumb { 30 | /*滚动条里面小方块*/ 31 | border-radius: 2px; 32 | -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); 33 | background: #535353; 34 | } 35 | 36 | ::-webkit-scrollbar-track { 37 | /*滚动条里面轨道*/ 38 | -webkit-box-shadow: inset 0 0 5px transparent; 39 | border-radius: 2px; 40 | background: #ededed; 41 | } 42 | 43 | /*# sourceMappingURL=v-im.cssss.map */ 44 | -------------------------------------------------------------------------------- /im-ui/src/components/chat/ChatTime.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /im-ui/src/components/chat/ChatVideoAcceptor.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 101 | 102 | 132 | -------------------------------------------------------------------------------- /im-ui/src/components/common/Emotion.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | 87 | -------------------------------------------------------------------------------- /im-ui/src/components/common/FileUpload.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 95 | 96 | 98 | -------------------------------------------------------------------------------- /im-ui/src/components/common/FullImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /im-ui/src/components/common/HeadImage.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 42 | 43 | 62 | -------------------------------------------------------------------------------- /im-ui/src/components/common/RightMenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 64 | -------------------------------------------------------------------------------- /im-ui/src/components/friend/AddFriend.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 87 | 88 | 118 | -------------------------------------------------------------------------------- /im-ui/src/components/friend/FriendItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 56 | 57 | 126 | -------------------------------------------------------------------------------- /im-ui/src/components/group/GroupItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | 35 | 72 | -------------------------------------------------------------------------------- /im-ui/src/components/group/GroupMember.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | 70 | -------------------------------------------------------------------------------- /im-ui/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import router from './router' 4 | import ElementUI from 'element-ui'; 5 | import 'element-ui/lib/theme-chalk/index.css'; 6 | import './assets/iconfont/iconfont.css'; 7 | import httpRequest from './api/httpRequest'; 8 | import * as socketApi from './api/wssocket'; 9 | import emotion from './api/emotion.js'; 10 | import element from './api/element.js'; 11 | import store from './store'; 12 | import * as enums from './api/enums.js'; 13 | import './utils/directive/dialogDrag'; 14 | 15 | Vue.use(ElementUI); 16 | 17 | // 挂载全局 18 | Vue.prototype.$wsApi = socketApi; 19 | Vue.prototype.$http = httpRequest // http请求方法 20 | Vue.prototype.$emo = emotion; // emo表情 21 | Vue.prototype.$elm = element; // 元素操作 22 | Vue.prototype.$enums = enums; // 枚举 23 | Vue.config.productionTip = false; 24 | 25 | new Vue({ 26 | el: '#app', 27 | // 配置路由 28 | router, 29 | store, 30 | render: h=>h(App) 31 | }) 32 | -------------------------------------------------------------------------------- /im-ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Login from '../view/Login' 4 | import Register from '../view/Register' 5 | import Home from '../view/Home' 6 | // 安装路由 7 | Vue.use(VueRouter); 8 | 9 | // 配置导出路由 10 | export default new VueRouter({ 11 | routes: [{ 12 | path: "/", 13 | redirect: "/login" 14 | }, 15 | { 16 | name: "Login", 17 | path: '/login', 18 | component: Login 19 | }, 20 | { 21 | name: "Register", 22 | path: '/register', 23 | component: Register 24 | }, 25 | { 26 | name: "Home", 27 | path: '/home', 28 | component: Home, 29 | children:[ 30 | { 31 | name: "Chat", 32 | path: "/home/chat", 33 | component: () => import("../view/Chat"), 34 | }, 35 | { 36 | name: "Friends", 37 | path: "/home/friend", 38 | component: () => import("../view/Friend"), 39 | }, 40 | { 41 | name: "Friends", 42 | path: "/home/group", 43 | component: () => import("../view/Group"), 44 | } 45 | ] 46 | } 47 | ] 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /im-ui/src/store/friendStore.js: -------------------------------------------------------------------------------- 1 | import httpRequest from '../api/httpRequest.js' 2 | 3 | export default { 4 | 5 | state: { 6 | friends: [], 7 | activeIndex: -1, 8 | timer: null 9 | }, 10 | mutations: { 11 | initFriendStore(state) { 12 | httpRequest({ 13 | url: '/friend/list', 14 | method: 'get' 15 | }).then((friends) => { 16 | this.commit("setFriends",friends); 17 | this.commit("refreshOnlineStatus"); 18 | }) 19 | }, 20 | 21 | setFriends(state, friends) { 22 | state.friends = friends; 23 | }, 24 | updateFriend(state,friend){ 25 | state.friends.forEach((f,index)=>{ 26 | if(f.id==friend.id){ 27 | // 拷贝属性 28 | let online = state.friends[index].online; 29 | Object.assign(state.friends[index], friend); 30 | state.friends[index].online =online; 31 | } 32 | }) 33 | }, 34 | activeFriend(state, index) { 35 | state.activeIndex = index; 36 | }, 37 | removeFriend(state, index) { 38 | state.friends.splice(index, 1); 39 | if(state.activeIndex >= state.friends.length){ 40 | state.activeIndex = state.friends.length-1; 41 | } 42 | }, 43 | addFriend(state, friend) { 44 | state.friends.push(friend); 45 | }, 46 | refreshOnlineStatus(state){ 47 | let userIds = []; 48 | console.log("refreshOnlineStatus") 49 | if(state.friends.length ==0){ 50 | return; 51 | } 52 | state.friends.forEach((f)=>{userIds.push(f.id)}); 53 | httpRequest({ 54 | url: '/user/online', 55 | method: 'get', 56 | params: {userIds: userIds.join(',')} 57 | }).then((onlineIds) => { 58 | this.commit("setOnlineStatus",onlineIds); 59 | }) 60 | 61 | // 30s后重新拉取 62 | clearTimeout(state.timer); 63 | state.timer = setTimeout(()=>{ 64 | this.commit("refreshOnlineStatus"); 65 | },30000) 66 | }, 67 | setOnlineStatus(state,onlineIds){ 68 | console.log("setOnlineStatus") 69 | state.friends.forEach((f)=>{ 70 | let onlineFriend = onlineIds.find((id)=> f.id==id); 71 | f.online = onlineFriend != undefined; 72 | }); 73 | 74 | let activeFriend = state.friends[state.activeIndex]; 75 | state.friends.sort((f1,f2)=>{ 76 | if(f1.online&&!f2.online){ 77 | return -1; 78 | } 79 | if(f2.online&&!f1.online){ 80 | return 1; 81 | } 82 | return 0; 83 | }); 84 | 85 | // 重新排序后,activeIndex指向的好友可能会变化,需要重新指定 86 | if(state.activeIndex >=0){ 87 | state.friends.forEach((f,i)=>{ 88 | if(f.id == activeFriend.id){ 89 | state.activeIndex = i; 90 | } 91 | }) 92 | } 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /im-ui/src/store/groupStore.js: -------------------------------------------------------------------------------- 1 | import httpRequest from '../api/httpRequest.js' 2 | 3 | export default { 4 | 5 | state: { 6 | groups: [], 7 | activeIndex: -1, 8 | }, 9 | mutations: { 10 | initGroupStore(state) { 11 | httpRequest({ 12 | url: '/group/list', 13 | method: 'get' 14 | }).then((groups) => { 15 | this.commit("setGroups",groups); 16 | }) 17 | }, 18 | setGroups(state,groups){ 19 | state.groups = groups; 20 | }, 21 | activeGroup(state,index){ 22 | state.activeIndex = index; 23 | }, 24 | addGroup(state,group){ 25 | state.groups.unshift(group); 26 | }, 27 | removeGroup(state,groupId){ 28 | state.groups.forEach((g,index)=>{ 29 | if(g.id==groupId){ 30 | state.groups.splice(index, 1); 31 | if(state.activeIndex >= state.groups.length){ 32 | state.activeIndex = state.groups.length-1; 33 | } 34 | } 35 | }) 36 | 37 | }, 38 | updateGroup(state,group){ 39 | state.groups.forEach((g,idx)=>{ 40 | if(g.id==group.id){ 41 | // 拷贝属性 42 | Object.assign(state.groups[idx], group); 43 | } 44 | }) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /im-ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import chatStore from './chatStore.js'; 4 | import friendStore from './friendStore.js'; 5 | import userStore from './userStore.js'; 6 | import groupStore from './groupStore.js'; 7 | import uiStore from './uiStore.js'; 8 | import VuexPersistence from 'vuex-persist' 9 | 10 | 11 | const vuexLocal = new VuexPersistence({ 12 | storage: window.localStorage, 13 | modules: ["userStore","chatStore"] 14 | }) 15 | 16 | Vue.use(Vuex) 17 | 18 | export default new Vuex.Store({ 19 | modules: {chatStore,friendStore,userStore,groupStore,uiStore}, 20 | state: {}, 21 | plugins: [vuexLocal.plugin], 22 | mutations: { 23 | initStore(state){ 24 | this.commit("initFriendStore"); 25 | this.commit("initGroupStore"); 26 | this.commit("initChatStore"); 27 | } 28 | 29 | }, 30 | strict: process.env.NODE_ENV !== 'production' 31 | }) 32 | -------------------------------------------------------------------------------- /im-ui/src/store/uiStore.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | userInfo: { // 用户信息窗口 4 | show: false, 5 | user: {}, 6 | pos:{ 7 | x:0, 8 | y:0 9 | } 10 | }, 11 | fullImage: { // 全屏大图 12 | show: false, 13 | url: "" 14 | }, 15 | chatPrivateVideo:{ // 私人视频聊天 16 | show: false, 17 | master: false, // 是否房主 18 | friend:{}, 19 | offer:{} // 对方发起带过过来的sdp信息 20 | }, 21 | videoAcceptor:{ // 视频呼叫选择 22 | show: false, 23 | 24 | friend:{} 25 | } 26 | 27 | }, 28 | mutations: { 29 | showUserInfoBox(state,user){ 30 | state.userInfo.show = true; 31 | state.userInfo.user = user; 32 | 33 | }, 34 | setUserInfoBoxPos(state,pos){ 35 | let w = document.documentElement.clientWidth; 36 | let h = document.documentElement.clientHeight; 37 | state.userInfo.pos.x = Math.min(pos.x,w-350); 38 | state.userInfo.pos.y = Math.min(pos.y,h-200); 39 | }, 40 | closeUserInfoBox(state){ 41 | state.userInfo.show = false; 42 | }, 43 | showFullImageBox(state,url){ 44 | state.fullImage.show = true; 45 | state.fullImage.url = url; 46 | }, 47 | closeFullImageBox(state){ 48 | state.fullImage.show = false; 49 | }, 50 | showChatPrivateVideoBox(state,info){ 51 | state.chatPrivateVideo.show = true; 52 | state.chatPrivateVideo.friend = info.friend; 53 | state.chatPrivateVideo.master = info.master; 54 | state.chatPrivateVideo.offer = info.offer; 55 | }, 56 | closeChatPrivateVideoBox(state){ 57 | state.chatPrivateVideo.show = false; 58 | }, 59 | showVideoAcceptorBox(state,friend){ 60 | state.videoAcceptor.show = true; 61 | state.videoAcceptor.friend = friend; 62 | 63 | }, 64 | closeVideoAcceptorBox(state){ 65 | state.videoAcceptor.show = false; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /im-ui/src/store/userStore.js: -------------------------------------------------------------------------------- 1 | import {USER_STATE} from "../api/enums.js" 2 | 3 | export default { 4 | 5 | state: { 6 | userInfo: { 7 | 8 | }, 9 | state: USER_STATE.FREE 10 | }, 11 | 12 | mutations: { 13 | setUserInfo(state, userInfo) { 14 | // 切换用户后,清理缓存 15 | if(userInfo.id != state.userInfo.id){ 16 | console.log("用户切换") 17 | this.commit("resetChatStore"); 18 | } 19 | state.userInfo = userInfo; 20 | }, 21 | setUserState(state, userState) { 22 | state.state = userState; 23 | }, 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /im-ui/src/utils/directive/dialogDrag.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 |   3 | // v-dialogDrag: 弹窗拖拽 4 | Vue.directive('dialogDrag', { 5 |   bind (el, binding, vnode, oldVnode) { 6 |     const dialogHeaderEl = el.querySelector('.el-dialog__header') 7 |     const dragDom = el.querySelector('.el-dialog') 8 |     dialogHeaderEl.style.cursor = 'move' 9 |   10 |     // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 11 |     const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null) 12 |   13 |     dialogHeaderEl.onmousedown = (e) => { 14 |       // 鼠标按下,计算当前元素距离可视区的距离 15 |       const disX = e.clientX - dialogHeaderEl.offsetLeft 16 |       const disY = e.clientY - dialogHeaderEl.offsetTop 17 |       const screenWidth = document.body.clientWidth; // body当前宽度 18 |       const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取) 19 |       const dragDomWidth = dragDom.offsetWidth; // 对话框宽度 20 |       const dragDomheight = dragDom.offsetHeight; // 对话框高度 21 |       const minDragDomLeft = dragDom.offsetLeft; 22 |       const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; 23 |       const minDragDomTop = dragDom.offsetTop; 24 |       const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight; 25 |   26 |       // 获取到的值带px 正则匹配替换 27 |       let styL, styT 28 |   29 |       // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px 30 |       if (sty.left.includes('%')) { 31 |         styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100) 32 |         styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100) 33 |       } else { 34 |         styL = +sty.left.replace(/\px/g, '') 35 |         styT = +sty.top.replace(/\px/g, '') 36 |       } 37 |   38 |       document.onmousemove = function (e) { 39 |         // 获取body的页面可视宽高 40 |         // var clientHeight = document.documentElement.clientHeight || document.body.clientHeight 41 |         // var clientWidth = document.documentElement.clientWidth || document.body.clientWidth 42 |   43 |         // 通过事件委托,计算移动的距离 44 |         var l = e.clientX - disX 45 |         var t = e.clientY - disY 46 |   47 |         // 边界处理 48 |         if (-l > minDragDomLeft) { 49 |           l = -minDragDomLeft; 50 |         } else if (l > maxDragDomLeft) { 51 |           l = maxDragDomLeft; 52 |         } 53 |         if (-t > minDragDomTop) { 54 |           t = -minDragDomTop; 55 |         } else if (t > maxDragDomTop) { 56 |           t = maxDragDomTop; 57 |         } 58 |         // 移动当前元素 59 |         dragDom.style.left = `${l + styL}px` 60 |         dragDom.style.top = `${t + styT}px` 61 |   62 |         // 将此时的位置传出去 63 |         // binding.value({x:e.pageX,y:e.pageY}) 64 |       } 65 |   66 |       document.onmouseup = function (e) { 67 |         document.onmousemove = null 68 |         document.onmouseup = null 69 |       } 70 |     } 71 |   } 72 | }) 73 | -------------------------------------------------------------------------------- /im-ui/src/view/Chat.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 70 | 71 | 92 | -------------------------------------------------------------------------------- /im-ui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | devServer: { 4 | proxy: { 5 | '/api': { 6 | target: 'http://127.0.0.1:8888', 7 | changeOrigin: true, 8 | ws: false, 9 | pathRewrite: { 10 | '^/api': '' 11 | } 12 | } 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /pic/im_pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/pic/im_pull.png -------------------------------------------------------------------------------- /pic/im_push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merua0oo0/im/b796a80dfc0f722d419a451894f8ba899222f95b/pic/im_push.png --------------------------------------------------------------------------------