diff --git a/README.md b/README.md index 7df8fde..87f6af2 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,13 @@ #### 近期更新 -发布2.0版本,本次更新主要是加入了uniapp版本: +发布2.0版本,本次更新加入了uniapp版本: - 支持移动端和web端同时在线,多端消息同步 - 目前仅兼容h5和微信小程序,后续会继续兼容更多终端类型 -- 页面风格优化:表情包更新、自动生成文字头像等 - -感兴趣的小伙伴,可在下方扫码体验 - +- 聊天窗口加入已读未读显示 +- 群聊加入@功能 +- 界面风格升级,表情包更新、生成文字头像等 #### 在线体验 diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java index 2535784..2b0ce80 100644 --- a/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java @@ -7,6 +7,8 @@ import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; @Data @ApiModel("群聊消息DTO") @@ -16,8 +18,7 @@ public class GroupMessageDTO { @ApiModelProperty(value = "群聊id") private Long groupId; - - @Length(max=1024,message = "内容长度不得大于1024") + @Length(max=1024,message = "发送内容长度不得大于1024") @NotEmpty(message="发送内容不可为空") @ApiModelProperty(value = "发送内容") private String content; @@ -25,4 +26,8 @@ public class GroupMessageDTO { @NotNull(message="消息类型不可为空") @ApiModelProperty(value = "消息类型") private Integer type; + + @Size(max = 20,message = "一次最多只能@20个小伙伴哦") + @ApiModelProperty(value = "被@用户列表") + private List atUserIds; } diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java index a94bec1..fa5fb28 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java @@ -44,6 +44,17 @@ public class GroupMessage extends Model { @TableField("send_id") private Long sendId; + /** + * 发送用户昵称 + */ + @TableField("send_nick_name") + private String sendNickName; + + /** + * @用户列表 + */ + @TableField("at_user_ids") + private String atUserIds; /** * 发送内容 */ diff --git a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java index 3611045..a4966c0 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java @@ -18,7 +18,6 @@ public interface IGroupMemberService extends IService { List findUserIdsByGroupId(Long groupId); - boolean saveOrUpdateBatch(Long groupId,List members); void removeByGroupId(Long groupId); diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java index 5be6caf..9493133 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java @@ -1,5 +1,6 @@ package com.bx.implatform.service.impl; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; @@ -7,7 +8,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bx.imclient.IMClient; import com.bx.imcommon.contant.IMConstant; -import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.util.DateTimeUtils; import com.bx.implatform.vo.GroupMessageVO; import com.bx.imcommon.model.IMGroupMessage; @@ -28,16 +28,13 @@ import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; import com.bx.implatform.util.BeanUtils; import com.bx.implatform.dto.GroupMessageDTO; +import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -62,26 +59,33 @@ public class GroupMessageServiceImpl extends ServiceImpl userIds = groupMemberService.findUserIdsByGroupId(group.getId()); - if (!userIds.contains(session.getUserId())) { + // 是否在群聊里面 + GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId()); + if (Objects.isNull(member)||member.getQuit()) { throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息"); } + // 群聊成员列表 + List userIds = groupMemberService.findUserIdsByGroupId(group.getId()); + // 不用发给自己 + userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); // 保存消息 GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class); msg.setSendId(session.getUserId()); msg.setSendTime(new Date()); + msg.setSendNickName(member.getAliasName()); + if(CollectionUtil.isNotEmpty(dto.getAtUserIds())){ + msg.setAtUserIds(StrUtil.join(",",dto.getAtUserIds())); + } this.save(msg); - // 不用发给自己 - userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); // 群发 GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); + msgInfo.setAtUserIds(dto.getAtUserIds()); IMGroupMessage sendMessage = new IMGroupMessage<>(); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setRecvIds(userIds); @@ -112,7 +116,7 @@ public class GroupMessageServiceImpl extends ServiceImpl members = groupMemberService.findByUserId(session.getUserId()); List ids = members.stream().map(GroupMember::getGroupId).collect(Collectors.toList()); + if(CollectionUtil.isEmpty(ids)){ + return Collections.EMPTY_LIST; + } // 只能拉取最近1个月的 Date minDate = DateTimeUtils.addMonths(new Date(), -1); LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); @@ -210,7 +217,13 @@ public class GroupMessageServiceImpl extends ServiceImpl messages = this.list(wrapper); // 转成vo - List vos = messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList()); + List vos = messages.stream().map(m -> { + GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); + // 被@用户列表 + List atIds = Arrays.asList(StrUtil.split(m.getAtUserIds(),",")); + vo.setAtUserIds(atIds.stream().map(id->Long.parseLong(id)).collect(Collectors.toList())); + return vo; + }).collect(Collectors.toList()); // 消息状态,数据库没有存群聊的消息状态,需要从redis取 List keys = ids.stream() .map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString())) @@ -242,6 +255,16 @@ public class GroupMessageServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMessage::getGroupId, groupId) + .orderByDesc(GroupMessage::getId) + .last("limit 1") + .select(GroupMessage::getId); + GroupMessage message = this.getOne(wrapper); + if(Objects.isNull(message)){ + return; + } // 推送消息给自己的其他终端 GroupMessageVO msgInfo = new GroupMessageVO(); msgInfo.setType(MessageType.READED.code()); @@ -254,14 +277,7 @@ public class GroupMessageServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaQuery(); - wrapper.eq(GroupMessage::getGroupId, groupId) - .orderByDesc(GroupMessage::getId) - .last("limit 1") - .select(GroupMessage::getId); - GroupMessage message = this.getOne(wrapper); + // 记录已读消息位置 String key = StrUtil.join(":",RedisKey.IM_GROUP_READED_POSITION,groupId,session.getUserId()); redisTemplate.opsForValue().set(key, message.getId()); diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java index 623d64f..d8597d3 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java @@ -76,7 +76,6 @@ public class GroupServiceImpl extends ServiceImpl implements groupMember.setUserId(user.getId()); groupMember.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName()); groupMember.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark()); - groupMember.setHeadImage(user.getHeadImageThumb()); groupMemberService.save(groupMember); vo.setId(group.getId()); diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java index f00cccc..1f5beae 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java @@ -6,6 +6,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.Date; +import java.util.List; @Data public class GroupMessageVO { @@ -19,12 +20,18 @@ public class GroupMessageVO { @ApiModelProperty(value = " 发送者id") private Long sendId; + @ApiModelProperty(value = " 发送者昵称") + private String sendNickName; + @ApiModelProperty(value = "消息内容") private String content; @ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义") private Integer type; + @ApiModelProperty(value = "@用户列表") + private List atUserIds; + @ApiModelProperty(value = " 状态") private Integer status; diff --git a/im-platform/src/main/resources/application.yml b/im-platform/src/main/resources/application.yml index 07678d0..6c77e0b 100644 --- a/im-platform/src/main/resources/application.yml +++ b/im-platform/src/main/resources/application.yml @@ -29,11 +29,11 @@ mybatis-plus: # *.xml的具体路径 - classpath*:mapper/*.xml minio: - endpoint: http://127.0.0.1:9001 #内网地址 - public: http://127.0.0.1:9001 #外网访问地址 + endpoint: http://42.194.187.243:9001 #内网地址 + public: http://42.194.187.243:9001 #外网访问地址 accessKey: admin - secretKey: 12345678 - bucketName: box-im + secretKey: admin123456 + bucketName: box-im2 imagePath: image filePath: file @@ -43,7 +43,7 @@ webrtc: jwt: accessToken: - expireIn: 1800 #半个小时 + expireIn: 1800 #半个小时 secret: MIIBIjANBgkq refreshToken: expireIn: 604800 #7天 diff --git a/im-platform/src/main/resources/db/db.sql b/im-platform/src/main/resources/db/db.sql index 9cca837..ce67f13 100644 --- a/im-platform/src/main/resources/db/db.sql +++ b/im-platform/src/main/resources/db/db.sql @@ -67,7 +67,9 @@ create table `im_group_message`( `id` bigint not null auto_increment primary key comment 'id', `group_id` bigint not null comment '群id', `send_id` bigint not null comment '发送用户id', + `send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称', `content` text comment '发送内容', + `at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔', `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示' , `status` tinyint(1) DEFAULT 0 comment '状态 0:正常 2:撤回', `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', diff --git a/im-ui/src/api/emotion.js b/im-ui/src/api/emotion.js index 86af031..c391496 100644 --- a/im-ui/src/api/emotion.js +++ b/im-ui/src/api/emotion.js @@ -16,15 +16,25 @@ let textToImg = (emoText) => { let word = emoText.replace(/\#|\;/gi, ''); let idx = emoTextList.indexOf(word); if(idx==-1){ - return ""; + return emoText; } let url = require(`@/assets/emoji/${idx}.gif`); - return `` + return `` } +let textToUrl = (emoText) => { + let word = emoText.replace(/\#|\;/gi, ''); + let idx = emoTextList.indexOf(word); + if(idx==-1){ + return ""; + } + let url = require(`@/assets/emoji/${idx}.gif`); + return url; +} export default { emoTextList, transform, - textToImg + textToImg, + textToUrl } diff --git a/im-ui/src/api/wssocket.js b/im-ui/src/api/wssocket.js index e014edd..e89988d 100644 --- a/im-ui/src/api/wssocket.js +++ b/im-ui/src/api/wssocket.js @@ -1,19 +1,11 @@ var websock = null; let rec; //断线重连后,延迟5秒重新创建WebSocket连接 rec用来存储延迟请求的代码 let isConnect = false; //连接标识 避免重复连接 -let wsurl = ""; -let accessToken = ""; let messageCallBack = null; -let openCallBack = null; let closeCallBack = null -let init = (url,token) => { - wsurl = url; - accessToken = token; -}; - -let connect = () => { +let connect = (wsurl,accessToken) => { try { if (isConnect) { return; @@ -25,12 +17,9 @@ let connect = () => { if (sendInfo.cmd == 0) { heartCheck.start() console.log('WebSocket登录成功') - // 登录成功才算连接完成 - openCallBack && openCallBack(); } else if (sendInfo.cmd == 1) { // 重新开启心跳定时 heartCheck.reset(); - console.log("") } else { // 其他消息转发出去 console.log("收到消息:",sendInfo); @@ -59,16 +48,16 @@ let connect = () => { websock.onerror = function() { console.log('WebSocket连接发生错误') isConnect = false; //连接断开修改标识 - reConnect(); + reconnect(wsurl,accessToken); } } catch (e) { console.log("尝试创建连接失败"); - reConnect(); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接 + reconnect(wsurl,accessToken); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接 } }; //定义重连函数 -let reConnect = () => { +let reconnect = (wsurl,accessToken) => { console.log("尝试重新连接"); if (isConnect){ //如果已经连上就不在重连了 @@ -76,12 +65,12 @@ let reConnect = () => { } rec && clearTimeout(rec); rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连 - connect(); - }, 5000); + connect(wsurl,accessToken); + }, 15000); }; //设置关闭连接 -let close = () => { - websock && websock.close(); +let close = (code) => { + websock && websock.close(code); }; @@ -136,20 +125,15 @@ let onMessage = (callback) => { } -let onOpen = (callback) => { - openCallBack = callback; -} - let onClose = (callback) => { closeCallBack = callback; } // 将方法暴露出去 export { - init, connect, + reconnect, close, sendMessage, - onOpen, onMessage, onClose } diff --git a/im-ui/src/components/chat/ChatAtBox.vue b/im-ui/src/components/chat/ChatAtBox.vue new file mode 100644 index 0000000..9e1cbc7 --- /dev/null +++ b/im-ui/src/components/chat/ChatAtBox.vue @@ -0,0 +1,175 @@ + + + + + \ No newline at end of file diff --git a/im-ui/src/components/chat/ChatBox.vue b/im-ui/src/components/chat/ChatBox.vue index e4f06b4..82744db 100644 --- a/im-ui/src/components/chat/ChatBox.vue +++ b/im-ui/src/components/chat/ChatBox.vue @@ -1,79 +1,87 @@ + \ No newline at end of file diff --git a/im-ui/src/components/common/FileUpload.vue b/im-ui/src/components/common/FileUpload.vue index 5b2eafb..480038c 100644 --- a/im-ui/src/components/common/FileUpload.vue +++ b/im-ui/src/components/common/FileUpload.vue @@ -1,6 +1,6 @@ @@ -37,7 +37,7 @@ } }, methods: { - handleSuccess(res, file) { + onSuccess(res, file) { this.loading && this.loading.close(); if (res.code == 200) { this.$emit("success", res.data, file); @@ -46,7 +46,7 @@ this.$emit("fail", res, file); } }, - handleError(err, file) { + onError(err, file) { this.$emit("fail", err, file); }, beforeUpload(file) { diff --git a/im-ui/src/components/common/FullImage.vue b/im-ui/src/components/common/FullImage.vue index 8bfd15e..444833e 100644 --- a/im-ui/src/components/common/FullImage.vue +++ b/im-ui/src/components/common/FullImage.vue @@ -1,10 +1,10 @@ @@ -17,7 +17,7 @@ } }, methods: { - handleClose() { + onClose() { this.$emit("close"); } }, diff --git a/im-ui/src/components/common/RightMenu.vue b/im-ui/src/components/common/RightMenu.vue index f71f8ea..e7107f8 100644 --- a/im-ui/src/components/common/RightMenu.vue +++ b/im-ui/src/components/common/RightMenu.vue @@ -3,7 +3,7 @@
+ @click="onSelectMenu(item)"> {{item.name}} @@ -31,7 +31,7 @@ close() { this.$emit("close"); }, - handleSelectMenu(item) { + onSelectMenu(item) { this.$emit("select", item); } } diff --git a/im-ui/src/components/common/UserInfo.vue b/im-ui/src/components/common/UserInfo.vue index 98be670..436a5fa 100644 --- a/im-ui/src/components/common/UserInfo.vue +++ b/im-ui/src/components/common/UserInfo.vue @@ -18,8 +18,8 @@
- 发消息 - 加为好友 + 发消息 + 加为好友
@@ -47,7 +47,7 @@ } }, methods: { - handleSendMessage() { + onSendMessage() { let user = this.user; let chat = { type: 'PRIVATE', @@ -62,7 +62,7 @@ } this.$emit("close"); }, - handleAddFriend() { + onAddFriend() { this.$http({ url: "/friend/add", method: "post", diff --git a/im-ui/src/components/friend/AddFriend.vue b/im-ui/src/components/friend/AddFriend.vue index d84ba7b..6f8ccae 100644 --- a/im-ui/src/components/friend/AddFriend.vue +++ b/im-ui/src/components/friend/AddFriend.vue @@ -1,7 +1,7 @@