diff --git a/commom/src/main/java/com/bx/common/contant/Constant.java b/commom/src/main/java/com/bx/common/contant/Constant.java index 8e6b2dd..6c5f130 100644 --- a/commom/src/main/java/com/bx/common/contant/Constant.java +++ b/commom/src/main/java/com/bx/common/contant/Constant.java @@ -12,4 +12,6 @@ public class Constant { public static final long MAX_GROUP_MEMBER = 500; // 在线状态过期时间 600s public static final long ONLINE_TIMEOUT_SECOND = 600; + // 消息允许撤回时间 300s + public static final long ALLOW_RECALL_SECOND = 300; } diff --git a/commom/src/main/java/com/bx/common/enums/MessageStatusEnum.java b/commom/src/main/java/com/bx/common/enums/MessageStatusEnum.java index 1ee5ca6..be6997b 100644 --- a/commom/src/main/java/com/bx/common/enums/MessageStatusEnum.java +++ b/commom/src/main/java/com/bx/common/enums/MessageStatusEnum.java @@ -4,8 +4,8 @@ package com.bx.common.enums; public enum MessageStatusEnum { UNREAD(0,"未读"), - ALREADY_READ(1,"已读"); - + ALREADY_READ(1,"已读"), + RECALL(2,"已撤回"); private Integer code; diff --git a/commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java b/commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java index f2dc541..de53313 100644 --- a/commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java +++ b/commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java @@ -6,7 +6,8 @@ public enum MessageTypeEnum { TEXT(0,"文字"), FILE(1,"文件"), IMAGE(2,"图片"), - VIDEO(3,"视频"); + VIDEO(3,"视频"), + TIP(10,"系统提示"); private Integer code; diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java index 0d7a62b..aac8764 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java @@ -40,7 +40,7 @@ public class GroupController { @ApiOperation(value = "解散群聊",notes="解散群聊") @DeleteMapping("/delete/{groupId}") - public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ + public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ groupService.deleteGroup(groupId); return ResultUtils.success(); } @@ -72,14 +72,14 @@ public class GroupController { @ApiOperation(value = "退出群聊",notes="退出群聊") @DeleteMapping("/quit/{groupId}") - public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ + public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ groupService.quitGroup(groupId); return ResultUtils.success(); } @ApiOperation(value = "踢出群聊",notes="将用户踢出群聊") @DeleteMapping("/kick/{groupId}") - public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, + public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, @NotNull(message = "用户id不能为空") @RequestParam Long userId){ groupService.kickGroup(groupId,userId); return ResultUtils.success(); diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java index 8151303..99bac42 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java @@ -8,13 +8,10 @@ import com.bx.implatform.vo.GroupMessageVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.validation.Valid; - +import javax.validation.constraints.NotNull; @Api(tags = "群聊消息") @@ -28,11 +25,16 @@ public class GroupMessageController { @PostMapping("/send") @ApiOperation(value = "发送群聊消息",notes="发送群聊消息") - public Result register(@Valid @RequestBody GroupMessageVO vo){ - groupMessageService.sendMessage(vo); - return ResultUtils.success(); + public Result sendMessage(@Valid @RequestBody GroupMessageVO vo){ + return ResultUtils.success(groupMessageService.sendMessage(vo)); } + @DeleteMapping("/recall/{id}") + @ApiOperation(value = "撤回消息",notes="撤回群聊消息") + public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){ + groupMessageService.recallMessage(id); + return ResultUtils.success(); + } @PostMapping("/pullUnreadMessage") @ApiOperation(value = "拉取未读消息",notes="拉取未读消息") diff --git a/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java index 67f0487..5fd0286 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java @@ -8,12 +8,10 @@ import com.bx.implatform.vo.PrivateMessageVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import javax.validation.constraints.NotNull; @Api(tags = "私聊消息") @RestController @@ -24,12 +22,20 @@ public class PrivateMessageController { private IPrivateMessageService privateMessageService; @PostMapping("/send") - @ApiOperation(value = "发送消息",notes="发送单人消息") - public Result register(@Valid @RequestBody PrivateMessageVO vo){ - privateMessageService.sendMessage(vo); + @ApiOperation(value = "发送消息",notes="发送私聊消息") + public Result sendMessage(@Valid @RequestBody PrivateMessageVO vo){ + return ResultUtils.success(privateMessageService.sendMessage(vo)); + } + + + @DeleteMapping("/recall/{id}") + @ApiOperation(value = "撤回消息",notes="撤回私聊消息") + public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){ + privateMessageService.recallMessage(id); return ResultUtils.success(); } + @PostMapping("/pullUnreadMessage") @ApiOperation(value = "拉取未读消息",notes="拉取未读消息") public Result pullUnreadMessage(){ 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 2734aab..a94bec1 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 @@ -56,6 +56,12 @@ public class GroupMessage extends Model { @TableField("type") private Integer type; + /** + * 状态 + */ + @TableField("status") + private Integer status; + /** * 发送时间 */ diff --git a/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java b/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java index 991dc78..e38012e 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java @@ -51,7 +51,7 @@ public class PrivateMessage extends Model { private String content; /** - * 消息类型 + * 消息类型 0:文字 1:图片 2:文件 3:语音 10:撤回消息 */ @TableField("type") private Integer type; diff --git a/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java index 435d693..0bd85fb 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java @@ -8,7 +8,9 @@ import com.bx.implatform.vo.GroupMessageVO; public interface IGroupMessageService extends IService { - void sendMessage(GroupMessageVO vo); + Long sendMessage(GroupMessageVO vo); + + void recallMessage(Long id); void pullUnreadMessage(); } diff --git a/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java index 691b530..e6c8069 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java @@ -7,7 +7,9 @@ import com.bx.implatform.vo.PrivateMessageVO; public interface IPrivateMessageService extends IService { - void sendMessage(PrivateMessageVO vo); + Long sendMessage(PrivateMessageVO vo); + + void recallMessage(Long id); void pullUnreadMessage(); 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 3c62332..3036dab 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 @@ -2,7 +2,10 @@ package com.bx.implatform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.common.contant.Constant; import com.bx.common.contant.RedisKey; +import com.bx.common.enums.MessageStatusEnum; +import com.bx.common.enums.MessageTypeEnum; import com.bx.common.enums.ResultCode; import com.bx.common.model.im.GroupMessageInfo; import com.bx.common.util.BeanUtils; @@ -46,7 +49,7 @@ public class GroupMessageServiceImpl extends ServiceImpl> serverMap = new ConcurrentHashMap<>(); - userIds.parallelStream().forEach(id->{ - String key = RedisKey.IM_USER_SERVER_ID + id; - Integer serverId = (Integer)redisTemplate.opsForValue().get(key); - if(serverId != null){ - if(serverMap.containsKey(serverId)){ - serverMap.get(serverId).add(id); - }else { - List list = Collections.synchronizedList(new LinkedList()); - list.add(id); - serverMap.put(serverId,list); - } - } - }); - // 逐个server发送 - for (Map.Entry> entry : serverMap.entrySet()) { - GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); - msgInfo.setRecvIds(new LinkedList<>(entry.getValue())); - String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey(); - redisTemplate.opsForList().rightPush(key,msgInfo); + // 群发 + GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); + this.sendMessage(userIds,msgInfo); + log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}",userId,vo.getGroupId(),vo.getContent()); + return msg.getId(); + } + + + + + /** + * 撤回消息 + * + * @param id 消息id + */ + @Override + public void recallMessage(Long id) { + Long userId = SessionContext.getSession().getId(); + GroupMessage msg = this.getById(id); + if(msg == null){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息不存在"); } - log.info("发送群聊消息,发送id:{},群聊id:{}",userId,vo.getGroupId()); + if(msg.getSendId() != userId){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"这条消息不是由您发送,无法撤回"); + } + if(System.currentTimeMillis() - msg.getSendTime().getTime() > Constant.ALLOW_RECALL_SECOND * 1000){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回"); + } + // 判断是否在群里 + GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(),userId); + if(member == null){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法撤回消息"); + } + // 修改数据库 + msg.setStatus(MessageStatusEnum.RECALL.getCode()); + this.updateById(msg); + // 群发 + List userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId()); + GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); + msgInfo.setType(MessageTypeEnum.TIP.getCode()); + String content = String.format("'%s'撤回了一条消息",member.getAliasName()); + msgInfo.setContent(content); + msgInfo.setSendTime(new Date()); + this.sendMessage(userIds,msgInfo); + log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}",userId,msg.getGroupId(),msg.getContent()); } + /** * 异步拉取群聊消息,通过websocket异步推送 * @@ -111,7 +137,9 @@ public class GroupMessageServiceImpl extends ServiceImpl wrapper = new QueryWrapper(); - wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId()); + wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId()) + .gt(GroupMessage::getSendTime,member.getCreatedTime()) + .ne(GroupMessage::getStatus,MessageStatusEnum.RECALL.getCode()); if(maxReadedId!=null){ wrapper.lambda().gt(GroupMessage::getId,maxReadedId); } @@ -128,6 +156,35 @@ public class GroupMessageServiceImpl extends ServiceImpl userIds, GroupMessageInfo msgInfo){ + // 根据群聊每个成员所连的IM-server,进行分组 + Map> serverMap = new ConcurrentHashMap<>(); + userIds.parallelStream().forEach(id->{ + String key = RedisKey.IM_USER_SERVER_ID + id; + Integer serverId = (Integer)redisTemplate.opsForValue().get(key); + if(serverId != null){ + if(serverMap.containsKey(serverId)){ + serverMap.get(serverId).add(id); + }else { + // 此处需要加锁,否则list可以会被覆盖 + synchronized(serverMap){ + List list = Collections.synchronizedList(new LinkedList()); + list.add(id); + serverMap.put(serverId,list); + } + } + } + }); + // 逐个server发送 + for (Map.Entry> entry : serverMap.entrySet()) { + msgInfo.setRecvIds(new LinkedList<>(entry.getValue())); + String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey(); + redisTemplate.opsForList().rightPush(key,msgInfo); } } } 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 c5b9ceb..65da723 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 @@ -30,6 +30,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -273,6 +274,7 @@ public class GroupServiceImpl extends ServiceImpl implements groupMember.setAliasName(f.getFriendNickName()); groupMember.setRemark(group.getName()); groupMember.setHeadImage(f.getFriendHeadImage()); + groupMember.setCreatedTime(new Date()); groupMember.setQuit(false); return groupMember; }).collect(Collectors.toList()); diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java index e4c5721..95265a3 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java @@ -2,8 +2,10 @@ package com.bx.implatform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.common.contant.Constant; import com.bx.common.contant.RedisKey; import com.bx.common.enums.MessageStatusEnum; +import com.bx.common.enums.MessageTypeEnum; import com.bx.common.enums.ResultCode; import com.bx.common.model.im.PrivateMessageInfo; import com.bx.common.util.BeanUtils; @@ -36,10 +38,10 @@ public class PrivateMessageServiceImpl extends ServiceImpl Constant.ALLOW_RECALL_SECOND * 1000){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回"); + } + // 修改消息状态 + msg.setStatus(MessageStatusEnum.RECALL.getCode()); + this.updateById(msg); + // 获取对方连接的channelId + String key = RedisKey.IM_USER_SERVER_ID+msg.getRecvId(); + Integer serverId = (Integer)redisTemplate.opsForValue().get(key); + // 如果对方在线,将数据存储至redis,等待拉取推送 + if(serverId != null){ + String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId; + PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); + msgInfo.setType(MessageTypeEnum.TIP.getCode()); + msgInfo.setSendTime(new Date()); + msgInfo.setContent("对方撤回了一条消息"); + redisTemplate.opsForList().rightPush(sendKey,msgInfo); + } + log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}",msg.getSendId(),msg.getRecvId(),msg.getContent()); } /** @@ -90,6 +129,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl updateWrapper = new UpdateWrapper<>(); updateWrapper.lambda().eq(PrivateMessage::getId,msgId) + .eq(PrivateMessage::getStatus,MessageStatusEnum.UNREAD.getCode()) .set(PrivateMessage::getStatus, MessageStatusEnum.ALREADY_READ.getCode()); privateMessageService.update(updateWrapper); log.info("消息已读,id:{}",msgId); 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 c6f349a..ebfe425 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 @@ -22,7 +22,7 @@ public class GroupMessageVO { @ApiModelProperty(value = "发送内容") private String content; - @NotNull(message="发送内容不可为空") + @NotNull(message="消息类型不可为空") @ApiModelProperty(value = "消息类型") private Integer type; } diff --git a/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java index d1e5e1f..7efdcc4 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java +++ b/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java @@ -24,7 +24,7 @@ public class PrivateMessageVO { @ApiModelProperty(value = "发送内容") private String content; - @NotNull(message="发送内容不可为空") + @NotNull(message="消息类型不可为空") @ApiModelProperty(value = "消息类型") private Integer type; diff --git a/im-platform/src/main/resources/db/db.sql b/im-platform/src/main/resources/db/db.sql index 5c52c3a..5e2365b 100644 --- a/im-platform/src/main/resources/db/db.sql +++ b/im-platform/src/main/resources/db/db.sql @@ -30,8 +30,8 @@ create table `im_private_message`( `send_id` bigint not null comment '发送用户id', `recv_id` bigint not null comment '接收用户id', `content` text comment '发送内容', - `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件', - `status` tinyint(1) NOT NULL comment '状态 0:未读 1:已读 ', + `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示', + `status` tinyint(1) NOT NULL comment '状态 0:未读 1:已读 2:撤回', `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', key `idx_send_recv_id` (`send_id`,`recv_id`) )ENGINE=InnoDB CHARSET=utf8mb3 comment '私聊消息'; @@ -67,7 +67,8 @@ create table `im_group_message`( `group_id` bigint not null comment '群id', `send_id` bigint not null comment '发送用户id', `content` text comment '发送内容', - `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件', + `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 '发送时间', key `idx_group_id` (group_id) )ENGINE=InnoDB CHARSET=utf8mb3 comment '群消息'; diff --git a/im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java b/im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java index 69f9a1d..5c76aa3 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java @@ -2,7 +2,7 @@ package com.bx.imserver.websocket; import io.netty.channel.ChannelHandlerContext; -import java.util.*; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java b/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java index c1510e5..657c3f9 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java @@ -1,7 +1,7 @@ package com.bx.imserver.websocket.endecode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.bx.common.model.im.SendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; diff --git a/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java b/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java index ee3c03c..328f02a 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java @@ -1,7 +1,7 @@ package com.bx.imserver.websocket.endecode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.bx.common.model.im.SendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageEncoder; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; diff --git a/im-server/src/main/java/com/bx/imserver/websocket/processor/GroupMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/websocket/processor/GroupMessageProcessor.java index d7f8359..67ace27 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/processor/GroupMessageProcessor.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/processor/GroupMessageProcessor.java @@ -1,6 +1,7 @@ package com.bx.imserver.websocket.processor; import com.bx.common.contant.RedisKey; +import com.bx.common.enums.MessageTypeEnum; import com.bx.common.enums.WSCmdEnum; import com.bx.common.model.im.GroupMessageInfo; import com.bx.common.model.im.SendInfo; @@ -39,9 +40,11 @@ public class GroupMessageProcessor extends MessageProcessor { sendInfo.setData(data); channelCtx.channel().writeAndFlush(sendInfo); } - // 设置已读最大id - String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId; - redisTemplate.opsForValue().set(key,data.getId()); + if(data.getType() != MessageTypeEnum.TIP.getCode()){ + // 设置已读最大id + String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId; + redisTemplate.opsForValue().set(key,data.getId()); + } }else { log.error("未找到WS连接,发送者:{},群id:{},接收id:{},内容:{}",data.getSendId(),data.getGroupId(),data.getRecvIds()); } diff --git a/im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java b/im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java index 9ae18de..1701299 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java @@ -8,7 +8,6 @@ import com.bx.common.model.im.HeartbeatInfo; import com.bx.common.model.im.SendInfo; import com.bx.imserver.websocket.WebsocketServer; import io.netty.channel.ChannelHandlerContext; -import io.netty.util.Attribute; import io.netty.util.AttributeKey; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; diff --git a/im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java index 427ae29..c8513ba 100644 --- a/im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java +++ b/im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java @@ -1,9 +1,10 @@ package com.bx.imserver.websocket.processor; import com.bx.common.contant.RedisKey; +import com.bx.common.enums.MessageTypeEnum; import com.bx.common.enums.WSCmdEnum; -import com.bx.common.model.im.SendInfo; import com.bx.common.model.im.PrivateMessageInfo; +import com.bx.common.model.im.SendInfo; import com.bx.imserver.websocket.WebsocketChannelCtxHolder; import io.netty.channel.ChannelHandlerContext; import lombok.extern.slf4j.Slf4j; @@ -29,9 +30,12 @@ public class PrivateMessageProcessor extends MessageProcessor - <%= htmlWebpackPlugin.options.title %> + 盒子IM -
- diff --git a/im-ui/src/api/element.js b/im-ui/src/api/element.js index 8a83926..a92fde5 100644 --- a/im-ui/src/api/element.js +++ b/im-ui/src/api/element.js @@ -14,7 +14,17 @@ let fixLeft = (e) => { return offset } +let setTitleTip = (tip) => { + let title = process.env.VUE_APP_NAME; + if(tip){ + title = `(${tip})${title}`; + } + document.title =title; + +} + export default{ fixTop, - fixLeft + fixLeft, + setTitleTip } diff --git a/im-ui/src/assets/iconfont/iconfont.css b/im-ui/src/assets/iconfont/iconfont.css new file mode 100644 index 0000000..c76ce8c --- /dev/null +++ b/im-ui/src/assets/iconfont/iconfont.css @@ -0,0 +1,43 @@ +@font-face { + font-family: "iconfont"; /* Project id 3776657 */ + src: url('iconfont.woff2?t=1668665799410') format('woff2'), + url('iconfont.woff?t=1668665799410') format('woff'), + url('iconfont.ttf?t=1668665799410') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-biaoqing:before { + content: "\e60c"; +} + +.icon-youyinpin:before { + content: "\e649"; +} + +.icon-audio:before { + content: "\e800"; +} + +.icon-group_fill:before { + content: "\e7f4"; +} + +.icon-yinpin:before { + content: "\e68a"; +} + +.icon-emoji:before { + content: "\e6f6"; +} + +.icon-voiceprint:before { + content: "\e953"; +} + diff --git a/im-ui/src/assets/iconfont/iconfont.ttf b/im-ui/src/assets/iconfont/iconfont.ttf new file mode 100644 index 0000000..9a4d5b0 Binary files /dev/null and b/im-ui/src/assets/iconfont/iconfont.ttf differ diff --git a/im-ui/src/assets/iconfont/iconfont.woff b/im-ui/src/assets/iconfont/iconfont.woff new file mode 100644 index 0000000..cbd67e3 Binary files /dev/null and b/im-ui/src/assets/iconfont/iconfont.woff differ diff --git a/im-ui/src/assets/iconfont/iconfont.woff2 b/im-ui/src/assets/iconfont/iconfont.woff2 new file mode 100644 index 0000000..94a9411 Binary files /dev/null and b/im-ui/src/assets/iconfont/iconfont.woff2 differ diff --git a/im-ui/src/components/chat/ChatBox.vue b/im-ui/src/components/chat/ChatBox.vue index 3ba8913..e515f46 100644 --- a/im-ui/src/components/chat/ChatBox.vue +++ b/im-ui/src/components/chat/ChatBox.vue @@ -13,7 +13,9 @@
  • + :showName="showName(msgInfo)" :msgInfo="msgInfo" + @delete="deleteMessage" + @recall="recallMessage">
@@ -21,7 +23,7 @@
-
+
{ - let info = { - type: this.chat.type, - targetId: file.raw.targetId, - fileId: file.raw.uid, - content: JSON.stringify(res.data), - loadStatus: "ok" - } - this.$store.commit("handleFileUpload", info); + }).then((id) => { + msgInfo.loadStatus = 'ok'; + msgInfo.id = id; + this.$store.commit("insertMessage", msgInfo); }) }, handleImageFail(res, file) { - let info = { - type: this.chat.type, - targetId: file.raw.targetId, - fileId: file.raw.uid, - loadStatus: "fail" - } - this.$store.commit("handleFileUpload", info); + let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo)); + msgInfo.loadStatus = 'fail'; + this.$store.commit("insertMessage", msgInfo); }, handleImageBefore(file) { let url = URL.createObjectURL(file); @@ -135,6 +122,7 @@ thumbUrl: url } let msgInfo = { + id:0, fileId: file.uid, sendId: this.mine.id, content: JSON.stringify(data), @@ -149,8 +137,8 @@ this.$store.commit("insertMessage", msgInfo); // 滚动到底部 this.scrollToBottom(); - // 借助file对象保存对方id - file.targetId = this.chat.targetId; + // 借助file对象保存 + file.msgInfo = msgInfo; }, handleFileSuccess(res, file) { let data = { @@ -158,35 +146,22 @@ size: file.size, url: res.data } - let msgInfo = { - content: JSON.stringify(data), - type: 2 - } - // 填充对方id - this.setTargetId(msgInfo, this.chat.targetId); + let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo)); + msgInfo.content = JSON.stringify(data); this.$http({ url: this.messageAction, method: 'post', data: msgInfo - }).then(() => { - let info = { - type: this.chat.type, - targetId: file.raw.targetId, - fileId: file.raw.uid, - content: JSON.stringify(data), - loadStatus: "ok" - } - this.$store.commit("handleFileUpload", info); + }).then((id) => { + msgInfo.loadStatus = 'ok'; + msgInfo.id = id; + this.$store.commit("insertMessage", msgInfo); }) }, handleFileFail(res, file) { - let info = { - type: this.chat.type, - targetId: file.raw.targetId, - fileId: file.raw.uid, - loadStatus: "fail" - } - this.$store.commit("handleFileUpload", info); + let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo)); + msgInfo.loadStatus = 'fail'; + this.$store.commit("insertMessage", msgInfo); }, handleFileBefore(file) { let url = URL.createObjectURL(file); @@ -196,7 +171,7 @@ url: url } let msgInfo = { - fileId: file.uid, + id: 0, sendId: this.mine.id, content: JSON.stringify(data), sendTime: new Date().getTime(), @@ -210,8 +185,8 @@ this.$store.commit("insertMessage", msgInfo); // 滚动到底部 this.scrollToBottom(); - // 借助file对象保存对方id - file.targetId = this.chat.targetId; + // 借助file对象透传 + file.msgInfo = msgInfo; }, handleCloseSide() { this.showSide = false; @@ -232,7 +207,6 @@ }, showVoiceBox() { this.showVoice = true; - }, closeVoiceBox() { this.showVoice = false; @@ -248,8 +222,9 @@ url: this.messageAction, method: 'post', data: msgInfo - }).then(() => { + }).then((id) => { this.$message.success("发送成功"); + msgInfo.id = id; msgInfo.sendTime = new Date().getTime(); msgInfo.sendId = this.$store.state.userStore.userInfo.id; msgInfo.selfSend = true; @@ -270,7 +245,6 @@ } }, sendTextMessage() { - if (!this.sendText.trim()) { this.$message.error("不能发送空白信息"); return @@ -285,9 +259,10 @@ url: this.messageAction, method: 'post', data: msgInfo - }).then((data) => { + }).then((id) => { this.$message.success("发送成功"); this.sendText = ""; + msgInfo.id = id; msgInfo.sendTime = new Date().getTime(); msgInfo.sendId = this.$store.state.userStore.userInfo.id; msgInfo.selfSend = true; @@ -304,6 +279,35 @@ return false; } }, + deleteMessage(msgInfo){ + this.$confirm( '确认删除消息?','删除消息', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.$store.commit("deleteMessage",msgInfo); + }); + }, + recallMessage(msgInfo){ + this.$confirm('确认撤回消息?','撤回消息', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}` + this.$http({ + url: url, + method: 'delete' + }).then(() => { + this.$message.success("消息已撤回"); + msgInfo = JSON.parse(JSON.stringify(msgInfo)); + msgInfo.type = 10; + msgInfo.content = '你撤回了一条消息'; + this.$store.commit("insertMessage",msgInfo); + }) + }); + + }, loadGroup(groupId) { this.$http({ url: `/group/find/${groupId}`, @@ -382,7 +386,7 @@ watch: { chat: { handler(newChat, oldChat) { - if (newChat.targetId > 0 && (newChat.type != oldChat.type || newChat.targetId != oldChat.targetId)) { + if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || newChat.targetId != oldChat.targetId)) { if (this.chat.type == "GROUP") { this.loadGroup(this.chat.targetId); } else { @@ -391,10 +395,12 @@ this.scrollToBottom(); this.sendText = ""; // 保持输入框焦点 - this.$refs.sendBox.focus(); + this.$nextTick(() => { + this.$refs.sendBox.focus(); + }) } }, - deep: true + immediate: true } } } @@ -431,7 +437,7 @@ border: #dddddd solid 1px; .im-chat-box { - ul { + >ul { padding: 20px; li { diff --git a/im-ui/src/components/chat/ChatVoice.vue b/im-ui/src/components/chat/ChatVoice.vue index cf3ff97..223eb49 100644 --- a/im-ui/src/components/chat/ChatVoice.vue +++ b/im-ui/src/components/chat/ChatVoice.vue @@ -96,6 +96,7 @@ }, handleRestartRecord() { this.rc.destroy(); + this.rc = new Recorder() this.rc.start(); this.state = 'RUNNING'; this.mode = 'RECORD'; diff --git a/im-ui/src/components/chat/MessageItem.vue b/im-ui/src/components/chat/MessageItem.vue index 9e89f7c..88d0317 100644 --- a/im-ui/src/components/chat/MessageItem.vue +++ b/im-ui/src/components/chat/MessageItem.vue @@ -1,58 +1,62 @@ + + diff --git a/im-ui/src/main.js b/im-ui/src/main.js index f68bb2b..5fb1b03 100644 --- a/im-ui/src/main.js +++ b/im-ui/src/main.js @@ -3,10 +3,11 @@ import App from './App' import router from './router' import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; +import './assets/iconfont/iconfont.css'; import httpRequest from './api/httpRequest'; import * as socketApi from './api/wssocket'; -import emotion from './api/emotion.js'; -import element from './api/element.js'; +import emotion from './api/emotion.js'; +import element from './api/element.js'; import store from './store'; diff --git a/im-ui/src/store/chatStore.js b/im-ui/src/store/chatStore.js index ca1debf..ed86801 100644 --- a/im-ui/src/store/chatStore.js +++ b/im-ui/src/store/chatStore.js @@ -7,7 +7,7 @@ export default { mutations: { initChatStore(state) { - state.activeIndex = -1; + //state.activeIndex = -1; }, openChat(state, chatInfo) { let chat = null; @@ -84,7 +84,7 @@ export default { break; } } - console.log(msgInfo.type) + // 插入新的数据 if(msgInfo.type == 1){ chat.lastContent = "[图片]"; }else if(msgInfo.type == 2){ @@ -95,20 +95,53 @@ export default { chat.lastContent = msgInfo.content; } chat.lastSendTime = msgInfo.sendTime; - chat.messages.push(msgInfo); // 如果不是当前会话,未读加1 chat.unreadCount++; if(msgInfo.selfSend){ chat.unreadCount=0; } + // 如果是已存在消息,则覆盖旧的消息数据 + for (let idx in chat.messages) { + if(msgInfo.id && chat.messages[idx].id == msgInfo.id){ + Object.assign(chat.messages[idx], msgInfo); + return; + } + // 正在发送中的消息可能没有id,通过发送时间判断 + if(msgInfo.selfSend && chat.messages[idx].selfSend + && chat.messages[idx].sendTime == msgInfo.sendTime){ + Object.assign(chat.messages[idx], msgInfo); + return; + } + } + // 新的消息 + chat.messages.push(msgInfo); + }, - handleFileUpload(state, info) { - // 文件上传后数据更新 - let chat = state.chats.find((c) => c.type==info.type && c.targetId === info.targetId); - let msg = chat.messages.find((m) => info.fileId == m.fileId); - msg.loadStatus = info.loadStatus; - if (info.content) { - msg.content = info.content; + deleteMessage(state, msgInfo){ + // 获取对方id或群id + let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; + let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; + let chat = null; + for (let idx in state.chats) { + if (state.chats[idx].type == type && + state.chats[idx].targetId === targetId) { + chat = state.chats[idx]; + break; + } + } + + for (let idx in chat.messages) { + // 已经发送成功的,根据id删除 + if(chat.messages[idx].id && chat.messages[idx].id == msgInfo.id){ + chat.messages.splice(idx, 1); + break; + } + // 正在发送中的消息可能没有id,根据发送时间删除 + if(msgInfo.selfSend && chat.messages[idx].selfSend + &&chat.messages[idx].sendTime == msgInfo.sendTime){ + chat.messages.splice(idx, 1); + break; + } } }, updateChatFromFriend(state, friend) { diff --git a/im-ui/src/store/friendStore.js b/im-ui/src/store/friendStore.js index 20ebeaf..3377c25 100644 --- a/im-ui/src/store/friendStore.js +++ b/im-ui/src/store/friendStore.js @@ -25,7 +25,7 @@ export default { state.friends.forEach((f,index)=>{ if(f.id==friend.id){ // 拷贝属性 - state.friends[index] = Object.assign(state.friends[index], friend); + Object.assign(state.friends[index], friend); } }) }, diff --git a/im-ui/src/store/groupStore.js b/im-ui/src/store/groupStore.js index 36e0bdc..ec1ede2 100644 --- a/im-ui/src/store/groupStore.js +++ b/im-ui/src/store/groupStore.js @@ -39,7 +39,7 @@ export default { state.groups.forEach((g,idx)=>{ if(g.id==group.id){ // 拷贝属性 - state.groups[idx] = Object.assign(state.groups[idx], group); + Object.assign(state.groups[idx], group); } }) } diff --git a/im-ui/src/store/uiStore.js b/im-ui/src/store/uiStore.js index a6ce3d2..351f6f4 100644 --- a/im-ui/src/store/uiStore.js +++ b/im-ui/src/store/uiStore.js @@ -12,6 +12,7 @@ export default { show: false, url: "" } + }, mutations: { showUserInfoBox(state,user){ diff --git a/im-ui/src/view/Home.vue b/im-ui/src/view/Home.vue index 85f6d84..f50ab7f 100644 --- a/im-ui/src/view/Home.vue +++ b/im-ui/src/view/Home.vue @@ -1,15 +1,16 @@ @@ -51,7 +46,7 @@ import Setting from '../components/setting/Setting.vue'; import UserInfo from '../components/common/UserInfo.vue'; import FullImage from '../components/common/FullImage.vue'; - + export default { components: { HeadImage, @@ -173,9 +168,26 @@ this.showSettingDialog = false; } }, - computed:{ - uiStore(){ + computed: { + uiStore() { return this.$store.state.uiStore; + }, + unreadCount() { + let unreadCount = 0; + let chats = this.$store.state.chatStore.chats; + chats.forEach((chat) => { + unreadCount += chat.unreadCount + }); + return unreadCount; + } + }, + watch: { + unreadCount: { + handler(newCount, oldCount) { + let tip = newCount > 0 ? `${newCount}条未读` : ""; + this.$elm.setTitleTip(tip); + }, + immediate: true } }, mounted() { @@ -209,12 +221,14 @@ flex: 1; .el-menu-item { - margin-top: 20px; + margin: 25px 0; .router-link-exact-active span { color: white !important; } + + span { font-size: 24px !important; color: #aaaaaa; @@ -223,6 +237,21 @@ color: white !important; } } + + .unread-text { + position: absolute; + line-height: 20px; + background-color: #f56c6c; + left: 36px; + top: 7px; + color: white; + border-radius: 30px; + padding: 0 5px; + font-size: 10px; + text-align: center; + white-space: nowrap; + border: 1px solid #f1e5e5; + } } } @@ -248,4 +277,4 @@ text-align: center; } - + diff --git a/im-ui/src/view/Login.vue b/im-ui/src/view/Login.vue index 7a43dfc..c25fa33 100644 --- a/im-ui/src/view/Login.vue +++ b/im-ui/src/view/Login.vue @@ -1,6 +1,6 @@