diff --git a/code/link/link-frontend/src/views/im/index.vue b/code/link/link-frontend/src/views/im/index.vue index b8a961b7..0dd39b98 100644 --- a/code/link/link-frontend/src/views/im/index.vue +++ b/code/link/link-frontend/src/views/im/index.vue @@ -287,14 +287,14 @@ async function loadFriends() { friends.value = friends.value.concat(records) } - // 设置当前聊天对象 - if (initialFriendId.value) { - const friend = friends.value.find(f => f.b_frienduser_id === initialFriendId.value) - if (friend) { - openPrivateChat(friend) + // 设置当前聊天对象 + if (initialFriendId.value) { + const friend = friends.value.find(f => f.b_frienduser_id === initialFriendId.value) + if (friend) { + openPrivateChat(friend) + } + initialFriendId.value = '' } - initialFriendId.value = '' - } friendsPage.value.total = total } diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/NettyServer.java b/code/link56/link56-backend/src/main/java/cn/link56/im/NettyServer.java index cb0be399..49fae9c3 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/NettyServer.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/NettyServer.java @@ -65,13 +65,13 @@ public class NettyServer { pipeline.addLast(new CorsHandler(CorsConfig.withAnyOrigin().build())); pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath(), null, true, 65536)); // 4. 空闲检测(60 秒没收到客户端任何消息就断) - pipeline.addLast("idle-handler", - new IdleStateHandler( - 60, - 0, - 0, - TimeUnit.SECONDS - )); +// pipeline.addLast("idle-handler", +// new IdleStateHandler( +// 60, +// 0, +// 0, +// TimeUnit.SECONDS +// )); pipeline.addLast(serverHandler); pipeline.addLast(authHandler); pipeline.addLast(dispatchHandler); diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/controller/ImController.java b/code/link56/link56-backend/src/main/java/cn/link56/im/controller/ImController.java new file mode 100644 index 00000000..938e1fbb --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/controller/ImController.java @@ -0,0 +1,55 @@ +package cn.link56.im.controller; + +import cn.link56.common.BasePageVO; +import cn.link56.common.Result; +import cn.link56.common.context.UserContext; +import cn.link56.im.model.dto.CreateFriendDTO; +import cn.link56.im.model.dto.QueryFriendDTO; +import cn.link56.im.model.dto.QueryGroupDTO; +import cn.link56.im.model.vo.QueryFriendVO; +import cn.link56.im.model.vo.QueryGroupVO; +import cn.link56.im.service.ImFriendService; +import cn.link56.im.service.ImGroupService; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.validation.annotation.Validated; +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 javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +@RestController +@RequestMapping("/im") +public class ImController { + @Resource + private ImFriendService imFriendService; + @Resource + private ImGroupService imGroupService; + + @PostMapping("/friend/create") + public Result createFriend(@RequestBody @Validated CreateFriendDTO dto){ + Long friendId = dto.getFriendId(); + Long userId = UserContext.getUserId(); + + if (!Objects.equals(friendId, userId)){ + imFriendService.createFriend(userId,friendId); + } + + return Result.success(); + } + + @PostMapping("/friend/query") + public Result> queryFriends(@RequestBody QueryFriendDTO dto){ + BasePageVO basePageVO = imFriendService.queryFriend(dto); + return Result.success(basePageVO); + } + + @PostMapping("/group/query") + public Result> queryFriends(@RequestBody QueryGroupDTO dto){ + BasePageVO basePageVO = imGroupService.queryGroup(dto); + return Result.success(basePageVO); + } +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/enums/MessageTypeEnums.java b/code/link56/link56-backend/src/main/java/cn/link56/im/enums/MessageTypeEnums.java index fe1a88d7..1381bcba 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/enums/MessageTypeEnums.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/enums/MessageTypeEnums.java @@ -9,9 +9,11 @@ public enum MessageTypeEnums { LOGIN(2), LOGOUT(3), ONLINE(4), - OFFLINE(5), - PRIVATE(6), - GROUP(7); + GROUP_ONLINE(5), + OFFLINE(6), + GROUP_OFFLINE(7), + PRIVATE(8), + GROUP(9); private final int code; diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/AuthHandler.java b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/AuthHandler.java index 1b3aef5d..0f62f1b2 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/AuthHandler.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/AuthHandler.java @@ -1,12 +1,9 @@ package cn.link56.im.handler; -import cn.link56.auth.token.model.TokenPayload; import cn.link56.auth.token.service.AuthTokenService; import cn.link56.im.enums.MessageTypeEnums; import cn.link56.im.message.MessageRequest; -import cn.link56.im.model.dto.OnlineDTO; import cn.link56.im.storage.SessionManager; -import com.alibaba.druid.util.StringUtils; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -33,13 +30,14 @@ public class AuthHandler extends ChannelInboundHandlerAdapter { MessageRequest req = (MessageRequest) msg; Integer type = req.getType(); - if (type == MessageTypeEnums.PING.getCode() || type == MessageTypeEnums.ONLINE.getCode()){ + if (type == MessageTypeEnums.PING.getCode() || type == MessageTypeEnums.LOGIN.getCode()){ ctx.fireChannelRead(req); return; } // 如果该通道未验证 if (!SessionManager.isAuthenticated(ctx.channel())) { + // 不断开连接 ctx.close(); return; } diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/MessageHandler.java b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/MessageHandler.java index 34a12b5d..156f401b 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/MessageHandler.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/MessageHandler.java @@ -5,33 +5,39 @@ import cn.link56.auth.token.service.AuthTokenService; import cn.link56.im.enums.MessageTypeEnums; import cn.link56.im.message.MessageRequest; import cn.link56.im.message.MessageResponse; -import cn.link56.im.model.dto.OnlineDTO; +import cn.link56.im.model.dto.LoginDTO; +import cn.link56.im.service.ImFriendService; +import cn.link56.im.service.ImGroupUserService; import cn.link56.im.storage.SessionManager; import cn.link56.im.utils.MessageDataConverter; import cn.link56.im.utils.MessageWriter; import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.util.List; @Component public class MessageHandler { @Resource private AuthTokenService authTokenService; + @Resource + private ImFriendService imFriendService; + @Resource + private ImGroupUserService imGroupUserService; // 处理消息 public void handleMessage(Channel channel, MessageRequest request) { - System.out.println(request); switch (MessageTypeEnums.valueOf(request.getType())) { case PING: handlePing(channel, request); break; - case ONLINE: - handleOnline(channel, request); + case LOGIN: + handleLogin(channel,request); break; - case OFFLINE: - handleOffline(channel, request); + case LOGOUT: + handleLogout(channel,request); break; case PRIVATE: handlePrivate(channel, request); @@ -49,9 +55,9 @@ public class MessageHandler { MessageWriter.writeToChannel(channel, MessageResponse.success(MessageTypeEnums.PONG)); } - // 上线 - private void handleOnline(Channel channel, MessageRequest request) { - OnlineDTO dto = MessageDataConverter.convert(request.getData(), OnlineDTO.class); + private void handleLogin(Channel channel, MessageRequest request) { + System.out.println("===LOGIN==="); + LoginDTO dto = MessageDataConverter.convert(request.getData(), LoginDTO.class); TokenPayload tokenPayload = authTokenService.parsePayload(dto.getToken()); if (tokenPayload == null) { @@ -65,11 +71,41 @@ public class MessageHandler { // 添加 Session 并自动标记已认证 SessionManager.addSession(companyId, userId, channel); - MessageWriter.writeToChannel(channel, MessageResponse.success(MessageTypeEnums.ONLINE)); + MessageWriter.writeToChannel(channel, MessageResponse.success(MessageTypeEnums.LOGIN)); + + handleOnline(channel,userId); + } + + private void handleLogout(Channel channel, MessageRequest request) { + System.out.println("===LOGOUT==="); + + Long userId = SessionManager.getUserId(channel); + if (ObjectUtils.isNotEmpty(userId)){ + handleOffline(channel , userId); + SessionManager.removeChannel(channel); + } + } + + // 上线:通知好友 + private void handleOnline(Channel channel, Long from) { + // 通知给好友 + List onlineFriends = imFriendService.getOnlineFriends(from); + MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.ONLINE).setFrom(from)); + + // 通知群组 + List onlineGroupUsers = imGroupUserService.getOnlineGroupUsers(from); + MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_ONLINE).setFrom(from)); } // 下线逻辑 - private void handleOffline(Channel channel, MessageRequest request) { + private void handleOffline(Channel channel, Long from) { + // 通知给好友 + List onlineFriends = imFriendService.getOnlineFriends(from); + MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.OFFLINE).setFrom(from)); + + // 通知群组 + List onlineGroupUsers = imGroupUserService.getOnlineGroupUsers(from); + MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_OFFLINE).setFrom(from)); } // 私聊逻辑 diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/ServerHandler.java b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/ServerHandler.java index c5c7c4fc..2f870803 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/handler/ServerHandler.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/handler/ServerHandler.java @@ -1,18 +1,27 @@ package cn.link56.im.handler; +import cn.link56.im.enums.MessageTypeEnums; import cn.link56.im.message.MessageRequest; +import cn.link56.im.storage.Session; import cn.link56.im.storage.SessionManager; import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.timeout.IdleStateEvent; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; +import javax.annotation.Resource; + @Component @ChannelHandler.Sharable public class ServerHandler extends SimpleChannelInboundHandler { + @Resource + private MessageHandler messageHandler; + private static final ObjectMapper MAPPER = new ObjectMapper(); @Override @@ -22,14 +31,32 @@ public class ServerHandler extends SimpleChannelInboundHandler { + + List selectFriend( + @Param("offset") int offset, + @Param("limit") int limit, + @Param("userId") Long userId, + @Param("name") String name, + @Param("id") Long id + ); + + Long selectFriendTotal(@Param("userId") Long userId); +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMapper.java b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMapper.java new file mode 100644 index 00000000..1340785c --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMapper.java @@ -0,0 +1,9 @@ +package cn.link56.im.mapper; + +import cn.link56.im.model.po.ImGroup; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ImGroupMapper extends BaseMapper { +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMessageMapper.java b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMessageMapper.java new file mode 100644 index 00000000..6e09b7a5 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupMessageMapper.java @@ -0,0 +1,9 @@ +package cn.link56.im.mapper; + +import cn.link56.im.model.po.ImGroupMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ImGroupMessageMapper extends BaseMapper { +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupUserMapper.java b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupUserMapper.java new file mode 100644 index 00000000..80fc7e6e --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImGroupUserMapper.java @@ -0,0 +1,9 @@ +package cn.link56.im.mapper; + +import cn.link56.im.model.po.ImGroupUser; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ImGroupUserMapper extends BaseMapper { +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImPrivateMessageMapper.java b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImPrivateMessageMapper.java new file mode 100644 index 00000000..aeb64386 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/mapper/ImPrivateMessageMapper.java @@ -0,0 +1,9 @@ +package cn.link56.im.mapper; + +import cn.link56.im.model.po.ImPrivateMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ImPrivateMessageMapper extends BaseMapper { +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageResponse.java b/code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageResponse.java index caa5e6c0..6a3a33c7 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageResponse.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageResponse.java @@ -9,42 +9,79 @@ import lombok.Data; import lombok.NoArgsConstructor; @Data -@AllArgsConstructor @NoArgsConstructor +@AllArgsConstructor @Builder public class MessageResponse { private Integer type; private Long from; private Object data; private Long timestamp; - private Integer code; private String message; - - public static MessageResponse success() { - return buildResponse(null, MessageResponseStatusConstants.SUCCESS, null, null); + // ---------------- 链式 Setter ---------------- + public MessageResponse setType(MessageTypeEnums type) { + this.type = type.getCode(); + return this; } - public static MessageResponse success(MessageTypeEnums messageTypeEnums) { - return buildResponse(messageTypeEnums.getCode(), MessageResponseStatusConstants.SUCCESS, null, null); + public MessageResponse setFrom(Long from) { + this.from = from; + return this; } - public static MessageResponse error() { - return buildResponse(null, MessageResponseStatusConstants.ERROR, null, "系统异常"); + public MessageResponse setData(Object data) { + this.data = data; + return this; + } + + public MessageResponse setTimestamp(Long timestamp) { + this.timestamp = timestamp; + return this; + } + + public MessageResponse setCode(Integer code) { + this.code = code; + return this; + } + + public MessageResponse setMessage(String message) { + this.message = message; + return this; + } + + // ---------------- 构建方法 ---------------- + public static MessageResponse success(MessageTypeEnums type) { + return new MessageResponse() + .setCode(MessageResponseStatusConstants.SUCCESS) + .setType(type) + .setTimestamp(TimeUtils.getCurrentTimestampMillis()); + } + + public static MessageResponse success(MessageTypeEnums type, Long from) { + return new MessageResponse() + .setCode(MessageResponseStatusConstants.SUCCESS) + .setType(type) + .setFrom(from) + .setTimestamp(TimeUtils.getCurrentTimestampMillis()); } public static MessageResponse error(String msg) { - return buildResponse(null, MessageResponseStatusConstants.ERROR, null, msg); + return new MessageResponse() + .setCode(MessageResponseStatusConstants.ERROR) + .setMessage(msg) + .setTimestamp(TimeUtils.getCurrentTimestampMillis()); } - private static MessageResponse buildResponse(Integer type, Integer code, Object data, String message) { - MessageResponse response = new MessageResponse(); - response.setType(type); - response.setCode(code); - response.setData(data); - response.setMessage(message); - response.setTimestamp(TimeUtils.getCurrentTimestampMillis()); - return response; + public static MessageResponse error(String msg, MessageTypeEnums type, Long from) { + return new MessageResponse() + .setCode(MessageResponseStatusConstants.ERROR) + .setMessage(msg) + .setType(type) + .setFrom(from) + .setTimestamp(TimeUtils.getCurrentTimestampMillis()); } + } + diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/CreateFriendDTO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/CreateFriendDTO.java new file mode 100644 index 00000000..b9d01374 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/CreateFriendDTO.java @@ -0,0 +1,15 @@ +package cn.link56.im.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateFriendDTO { + @NotNull(message = "好友ID不能为空") + private Long friendId; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/OnlineDTO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/LoginDTO.java similarity index 88% rename from code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/OnlineDTO.java rename to code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/LoginDTO.java index 40e16d94..c6b5f4da 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/OnlineDTO.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/LoginDTO.java @@ -7,6 +7,6 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class OnlineDTO { +public class LoginDTO { private String token; } diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryFriendDTO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryFriendDTO.java new file mode 100644 index 00000000..832322c1 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryFriendDTO.java @@ -0,0 +1,17 @@ +package cn.link56.im.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QueryFriendDTO { + private Integer currentPage; + private Integer pageSize; + + private String name; + + private Long id; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryGroupDTO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryGroupDTO.java new file mode 100644 index 00000000..c51ae4c9 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/dto/QueryGroupDTO.java @@ -0,0 +1,17 @@ +package cn.link56.im.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QueryGroupDTO { + private Integer currentPage; + private Integer pageSize; + + private String name; + + private Long id; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImFriend.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImFriend.java new file mode 100644 index 00000000..2128aa49 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImFriend.java @@ -0,0 +1,46 @@ +package cn.link56.im.model.po; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("b_im_friend") +@Accessors(chain = true) +public class ImFriend { + @TableId(value = "b_id") + private Long id; + + @TableField("b_user_id") + private Long userId; + + @TableField("b_friend_id") + private Long friendId; + + @TableField("b_unread_count") + private Integer unreadCount; + + @TableField("b_last_message_id") + private Long lastMessageId; + + @TableField( + value = "b_create_time", + fill = FieldFill.INSERT + ) + private Date createTime; + + @TableField( + value = "b_update_time", + fill = FieldFill.INSERT_UPDATE + ) + private Date updateTime; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroup.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroup.java new file mode 100644 index 00000000..cdda2f05 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroup.java @@ -0,0 +1,56 @@ +package cn.link56.im.model.po; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("b_im_group") +public class ImGroup { + @TableId(value = "b_id") + private Long id; + + @TableField("b_name") + private String name; + + @TableField("b_description") + private String description; + + @TableField("b_last_message_id") + private Long lastMessageId; + + @TableField("b_avatar") + private String avatar; + + @TableField( + value = "b_create_by", + fill = FieldFill.INSERT + ) + private Long createBy; + + @TableField( + value = "b_create_time", + fill = FieldFill.INSERT + ) + private Date createTime; + + @TableField( + value = "b_update_by", + fill = FieldFill.INSERT_UPDATE + ) + private Long updateBy; + + @TableField( + value = "b_update_time", + fill = FieldFill.INSERT_UPDATE + ) + private Date updateTime; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupMessage.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupMessage.java new file mode 100644 index 00000000..231e926c --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupMessage.java @@ -0,0 +1,32 @@ +package cn.link56.im.model.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("b_im_group_message") +public class ImGroupMessage { + @TableId(value = "b_id") + private Long id; + + @TableField("b_sender_id") + private Long senderId; + + @TableField("b_group_id") + private Long groupId; + + @TableField("b_content") + private String content; + + @TableField("b_type") + private Integer type; + + @TableField("b_send_time") + private Long sendTime; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupUser.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupUser.java new file mode 100644 index 00000000..b579247f --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImGroupUser.java @@ -0,0 +1,30 @@ +package cn.link56.im.model.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("b_im_group_user") +public class ImGroupUser { + @TableField("b_group_id") + private Long groupId; + + @TableField("b_user_id") + private Long userId; + + @TableField("b_join_time") + private Date joinTime; + + @TableField("b_last_active_time") + private Date lastActiveTime; + + @TableField("b_unread_count") + private Integer unreadCount; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImPrivateMessage.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImPrivateMessage.java new file mode 100644 index 00000000..1771d351 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/po/ImPrivateMessage.java @@ -0,0 +1,32 @@ +package cn.link56.im.model.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("b_im_private_message") +public class ImPrivateMessage { + @TableId(value = "b_id") + private Long id; + + @TableField("b_sender_id") + private Long senderId; + + @TableField("b_receiver_id") + private Long receiverId; + + @TableField("b_content") + private String content; + + @TableField("b_type") + private Integer type; + + @TableField("b_send_time") + private Long sendTime; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryFriendVO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryFriendVO.java new file mode 100644 index 00000000..36dc8e4e --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryFriendVO.java @@ -0,0 +1,27 @@ +package cn.link56.im.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QueryFriendVO { + private Long id; + + private Long friendId; + private String friendName; + private String friendAvatar; + + private Integer unreadCount; + + private Long lastMessageId; + private Long laseMessageTime; + private Integer lastMessageType; + private String lastMessageContent; + + private Boolean isOnline; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryGroupVO.java b/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryGroupVO.java new file mode 100644 index 00000000..a40bcb83 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/model/vo/QueryGroupVO.java @@ -0,0 +1,21 @@ +package cn.link56.im.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QueryGroupVO { + private Long id; + private String name; + private String avatar; + + private Integer unreadCount; + + private Long lastMessageId; + private Long laseMessageTime; + private Integer lastMessageType; + private String lastMessageContent; +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImFriendService.java b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImFriendService.java new file mode 100644 index 00000000..61b0acdb --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImFriendService.java @@ -0,0 +1,90 @@ +package cn.link56.im.service; + +import cn.link56.common.BasePageVO; +import cn.link56.common.context.UserContext; +import cn.link56.common.utils.snowflake.idgen.IdGenerator; +import cn.link56.im.mapper.ImFriendMapper; +import cn.link56.im.model.dto.QueryFriendDTO; +import cn.link56.im.model.po.ImFriend; +import cn.link56.im.model.vo.QueryFriendVO; +import cn.link56.im.storage.SessionManager; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ImFriendService extends ServiceImpl { + @Resource + private ImFriendMapper imFriendMapper; + + public void createFriend(Long userId,Long friendId){ + // 先检查是否存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ImFriend::getUserId,userId) + .eq(ImFriend::getFriendId,friendId); + Long selectCount = imFriendMapper.selectCount(queryWrapper); + + if (selectCount > 0){ + return; + } + + + // 不存在则插入 + ImFriend imFriend = new ImFriend(); + + imFriend.setId(IdGenerator.nextId()) + .setUserId(userId) + .setFriendId(friendId) + .setUnreadCount(0); + + imFriendMapper.insert(imFriend); + } + + public BasePageVO queryFriend(QueryFriendDTO dto){ + Long userId = UserContext.getUserId(); + + String name = dto.getName(); + Long id = dto.getId(); + + int offset = (dto.getCurrentPage() - 1) * dto.getPageSize() + 1; + int limit = dto.getCurrentPage() * dto.getPageSize(); + + // 分页查询 + List queryFriendVOS = imFriendMapper.selectFriend(offset, limit,userId, name, id); + + queryFriendVOS.forEach(item -> { + Long friendId = item.getFriendId(); + + item.setIsOnline(SessionManager.isOnline(friendId)); + }); + // 查询总数 + Long total = imFriendMapper.selectFriendTotal(userId); + + BasePageVO basePageVO = new BasePageVO<>(); + basePageVO.setRecords(queryFriendVOS); + basePageVO.setTotal(total); + + return basePageVO; + } + + // 获取在线好友ID列表 + public List getOnlineFriends(Long userId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ImFriend::getUserId, userId) + .select(ImFriend::getFriendId); + + List friends = imFriendMapper.selectList(queryWrapper); + + return friends.stream() + .map(ImFriend::getFriendId) + .filter(SessionManager::isOnline) + .collect(Collectors.toList()); + } + + +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupMessageService.java b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupMessageService.java new file mode 100644 index 00000000..f5de34a0 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupMessageService.java @@ -0,0 +1,11 @@ +package cn.link56.im.service; + +import cn.link56.im.mapper.ImGroupMessageMapper; +import cn.link56.im.model.po.ImGroupMessage; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +@Service +public class ImGroupMessageService extends ServiceImpl { + +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupService.java b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupService.java new file mode 100644 index 00000000..aa5d46d0 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupService.java @@ -0,0 +1,28 @@ +package cn.link56.im.service; + +import cn.link56.common.BasePageVO; +import cn.link56.im.mapper.ImGroupMapper; +import cn.link56.im.model.dto.QueryGroupDTO; +import cn.link56.im.model.po.ImGroup; +import cn.link56.im.model.vo.QueryGroupVO; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class ImGroupService extends ServiceImpl{ + @Resource + private ImGroupMapper imGroupMapper; + + public BasePageVO queryGroup(QueryGroupDTO dto) { + String name = dto.getName(); + Long id = dto.getId(); + + int offset = (dto.getCurrentPage() - 1) * dto.getPageSize() + 1; + int limit = dto.getCurrentPage() * dto.getPageSize(); + + + return new BasePageVO<>(); + } +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupUserService.java b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupUserService.java new file mode 100644 index 00000000..2294712e --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImGroupUserService.java @@ -0,0 +1,47 @@ +package cn.link56.im.service; + +import cn.link56.im.mapper.ImGroupUserMapper; +import cn.link56.im.model.po.ImGroupUser; +import cn.link56.im.storage.SessionManager; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ImGroupUserService extends ServiceImpl { + @Resource + private ImGroupUserMapper imGroupUserMapper; + + // 获取群组在线用户列表 + public List getOnlineGroupUsers(Long userId) { + // 1️⃣ 查询用户所在的群组ID列表 + LambdaQueryWrapper groupWrapper = new LambdaQueryWrapper<>(); + groupWrapper.eq(ImGroupUser::getUserId, userId) + .select(ImGroupUser::getGroupId); + List userGroups = imGroupUserMapper.selectList(groupWrapper); + List groupIds = userGroups.stream() + .map(ImGroupUser::getGroupId) + .collect(Collectors.toList()); + + if (groupIds.isEmpty()) return Collections.emptyList(); + + // 2️⃣ 查询这些群组的成员ID + LambdaQueryWrapper memberWrapper = new LambdaQueryWrapper<>(); + memberWrapper.in(ImGroupUser::getGroupId, groupIds) + .select(ImGroupUser::getUserId); + List groupMembers = imGroupUserMapper.selectList(memberWrapper); + + // 3️⃣ 过滤在线用户 + + return groupMembers.stream() + .map(ImGroupUser::getUserId) + .filter(SessionManager::isOnline) // 内存判断在线 + .distinct() // 去重 + .collect(Collectors.toList()); + } +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImPrivateMessageService.java b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImPrivateMessageService.java new file mode 100644 index 00000000..dab3a364 --- /dev/null +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/service/ImPrivateMessageService.java @@ -0,0 +1,10 @@ +package cn.link56.im.service; + +import cn.link56.im.mapper.ImPrivateMessageMapper; +import cn.link56.im.model.po.ImPrivateMessage; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +@Service +public class ImPrivateMessageService extends ServiceImpl { +} diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/storage/SessionManager.java b/code/link56/link56-backend/src/main/java/cn/link56/im/storage/SessionManager.java index 2c6064a9..f27eae37 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/storage/SessionManager.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/storage/SessionManager.java @@ -6,7 +6,6 @@ import io.netty.util.AttributeKey; import java.util.concurrent.ConcurrentHashMap; public class SessionManager { - /** userId -> Session */ private static final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); diff --git a/code/link56/link56-backend/src/main/java/cn/link56/im/utils/MessageWriter.java b/code/link56/link56-backend/src/main/java/cn/link56/im/utils/MessageWriter.java index d50fd787..7931836c 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/im/utils/MessageWriter.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/im/utils/MessageWriter.java @@ -42,8 +42,6 @@ public class MessageWriter { String payload = MAPPER.writeValueAsString(message); // 发送给客户端 channel.writeAndFlush(new TextWebSocketFrame(payload)); - System.out.println("===发送消息==="); - System.out.println(payload); } catch (Exception e) { System.out.println("===发送消息失败==="); e.printStackTrace(); diff --git a/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImFriendMapper.xml b/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImFriendMapper.xml new file mode 100644 index 00000000..4101bbe3 --- /dev/null +++ b/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImFriendMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImGroupMapper.xml b/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImGroupMapper.xml new file mode 100644 index 00000000..16044e20 --- /dev/null +++ b/code/link56/link56-backend/src/main/resources/cn/link56/im/mapper/ImGroupMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/code/link56/link56-frontend-new/src/App.vue b/code/link56/link56-frontend-new/src/App.vue index 924c2010..dda86bd2 100644 --- a/code/link56/link56-frontend-new/src/App.vue +++ b/code/link56/link56-frontend-new/src/App.vue @@ -4,25 +4,20 @@ import { onBeforeUnmount, onMounted } from 'vue' import { initBaseApi } from '@/request/api/base' import { useCommonStore } from '@/store/common' import { getPostTagApi } from './request/api/post' -import { useUserStore } from './store/user' import { useWebSocket } from './store/websocket' const websocket = useWebSocket() const commonOptions = useCommonStore() -const userStore = useUserStore() onMounted(() => { websocket.connect() - if (userStore.isLogined) { - // websocket.setAuthToken(userStore.token) - websocket.setAuthToken("123") - } - handleInitData() }) onBeforeUnmount(() => { + websocket.logout() + websocket.close() }) diff --git a/code/link56/link56-frontend-new/src/request/api/im/index.ts b/code/link56/link56-frontend-new/src/request/api/im/index.ts new file mode 100644 index 00000000..20830737 --- /dev/null +++ b/code/link56/link56-frontend-new/src/request/api/im/index.ts @@ -0,0 +1,10 @@ +import type { CreateFriendParams, ImFriend, QueryFriendParams } from '@/types/im' +import { httpPostJson } from '@/request/utils' + +export function createFriendApi(params: CreateFriendParams) { + return httpPostJson(`/im/friend/create`, params) +} + +export function queryFriendApi(params: QueryFriendParams) { + return httpPostJson>('/im/friend/query', params) +} diff --git a/code/link56/link56-frontend-new/src/store/websocket.ts b/code/link56/link56-frontend-new/src/store/websocket.ts index 965db430..1c143e00 100644 --- a/code/link56/link56-frontend-new/src/store/websocket.ts +++ b/code/link56/link56-frontend-new/src/store/websocket.ts @@ -1,6 +1,7 @@ import type { MessageRequest, MessageResponse } from '@/types/im' import { readonly, ref } from 'vue' import { MessageType } from '@/types/im' +import { useUserStore } from './user' interface UseWebSocketOptions { url: string @@ -14,6 +15,7 @@ interface UseWebSocketOptions { let singleton: ReturnType | null = null function createWebSocket(options?: Partial) { + const userStore = useUserStore() /* ================== 配置 ================== */ const config: Required = { url: @@ -33,7 +35,7 @@ function createWebSocket(options?: Partial) { const connected = ref(false) const authenticated = ref(false) - let cachedToken: string | null = null + let cachedToken: string | null = userStore.token let heartbeatTimer: number | null = null let pongTimeoutTimer: number | null = null @@ -75,6 +77,10 @@ function createWebSocket(options?: Partial) { ws.onmessage = (evt) => { const msg: MessageResponse = JSON.parse(evt.data) + + console.log('===收到消息===') + console.log(msg) + handleMessage(msg) } @@ -178,7 +184,7 @@ function createWebSocket(options?: Partial) { const msg: MessageRequest = { type, - from: 0, + from: userStore.user.id, to: to ?? null, timestamp: Date.now(), data: data ?? null, @@ -196,15 +202,9 @@ function createWebSocket(options?: Partial) { } /* ================== 登录相关 ================== */ - - function setAuthToken(token: string | null) { - cachedToken = token - if (token && connected.value) { - login(token) - } - } - function login(token: string) { + console.log('触发login:', authenticated.value) + if (!connected.value || authenticated.value) return cachedToken = token @@ -214,7 +214,6 @@ function createWebSocket(options?: Partial) { function logout() { cachedToken = null authenticated.value = false - sendRaw(MessageType.LOGOUT) } /* ================== 消息处理(核心修改点) ================== */ @@ -264,7 +263,6 @@ function createWebSocket(options?: Partial) { return { connect, close, - setAuthToken, login, logout, diff --git a/code/link56/link56-frontend-new/src/types/company/index.ts b/code/link56/link56-frontend-new/src/types/company/index.ts index e3c80dbb..510b055f 100644 --- a/code/link56/link56-frontend-new/src/types/company/index.ts +++ b/code/link56/link56-frontend-new/src/types/company/index.ts @@ -33,6 +33,16 @@ export interface Company { email: string } +export interface CompanyDetailUser { + id: number + companyId: number + phone: string + email: string + role: number + position: string + name: string +} + export interface CompanyDetail { id: number name: string @@ -44,15 +54,7 @@ export interface CompanyDetail { email: string description: string establishmentDate: string - users: { - id: number - companyId: number - phone: string - email: string - role: number - position: string - name: string - }[] + users: CompanyDetailUser[] } export interface UpdateCompanyParams { diff --git a/code/link56/link56-frontend-new/src/types/im/index.ts b/code/link56/link56-frontend-new/src/types/im/index.ts index a7b7008b..21091134 100644 --- a/code/link56/link56-frontend-new/src/types/im/index.ts +++ b/code/link56/link56-frontend-new/src/types/im/index.ts @@ -1,3 +1,4 @@ +// WebSocket 相关 export enum MessageType { PING = 0, PONG = 1, @@ -6,10 +7,12 @@ export enum MessageType { LOGOUT = 3, ONLINE = 4, - OFFLINE = 5, + GROUP_ONLINE = 5, + OFFLINE = 6, + GROUP_OFFLINE = 7, - PRIVATE = 6, - GROUP = 7, + PRIVATE = 8, + GROUP = 9, } export interface MessageRequest { @@ -29,3 +32,33 @@ export interface MessageResponse { code: number message: string } + +// http 相关 +export interface CreateFriendParams { + friendId: number +} + +export interface QueryFriendParams { + currentPage: number + pageSize: number + + name: string + id: number | null +} + +export interface ImFriend { + id: number + + friendId: number + friendName: string + friendAvatar: string + + unreadCount: number + + lastMessageId: number + lastMessageTime: number + lastMessageType: number + lastMessageContent: string + + isOnline: boolean +} diff --git a/code/link56/link56-frontend-new/src/utils/time.ts b/code/link56/link56-frontend-new/src/utils/time.ts index 1ff4e70d..f3bb96f6 100644 --- a/code/link56/link56-frontend-new/src/utils/time.ts +++ b/code/link56/link56-frontend-new/src/utils/time.ts @@ -113,3 +113,54 @@ export function formatDate( return date.format(format) } + +/** + * 格式化聊天时间 + * @param timestamp - 毫秒级时间戳 + * @param mode - 显示模式:'list' 为列表,'chat' 为聊天记录 + * @returns 格式化后的时间字符串 + */ +export function formatImTime(timestamp: number | null | undefined, mode: 'list' | 'chat' = 'list'): string { + if (!timestamp) + return '' + + const now = new Date() + const date = new Date(timestamp) + + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + + // 去除时分秒的日期 + const nowMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const dateMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const diffDays = Math.floor((nowMidnight.getTime() - dateMidnight.getTime()) / (1000 * 3600 * 24)) + + // 1️⃣ 当天 + if (diffDays === 0) + return `${hour}:${minute}` + + // 2️⃣ 昨天 + if (diffDays === 1) + return `昨天 ${hour}:${minute}` + + // 3️⃣ 一周内(周一到周日) + const weekStart = new Date(nowMidnight.getTime() - ((now.getDay() || 7) - 1) * 24 * 3600 * 1000) + if (dateMidnight >= weekStart) { + return mode === 'chat' + ? `${weekDays[date.getDay()]} ${hour}:${minute}` + : weekDays[date.getDay()] + } + + // 4️⃣ 本年或跨年(聊天记录模式:统一显示 YYYY/MM/DD HH:mm) + const ymd = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}` + + if (mode === 'chat') + return `${ymd} ${hour}:${minute}` + + // 列表模式区分本年/跨年 + if (date.getFullYear() === now.getFullYear()) + return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}` + + return ymd +} diff --git a/code/link56/link56-frontend-new/src/views/company/detail.vue b/code/link56/link56-frontend-new/src/views/company/detail.vue index 8dbc1a2e..5b8e4632 100644 --- a/code/link56/link56-frontend-new/src/views/company/detail.vue +++ b/code/link56/link56-frontend-new/src/views/company/detail.vue @@ -1,6 +1,6 @@