netty实现私信聊天

websocket的介绍:
WebSocket是一种在网络通信中的协议,它是独立于HTTP协议的。该协议基于TCP/IP协议,可以提供双向通讯并保有状态。这意味着客户端和服务器可以进行实时响应,并且这种响应是双向的。WebSocket协议端口通常是80,443。

WebSocket的出现使得浏览器具备了实时双向通信的能力。与HTTP这种非持久单向响应应答的协议相比,WebSocket是一个持久化的协议。举例来说,即使在关闭网页或者浏览器后,WebSocket的连接仍然保持,用户也可以继续接收到服务器的消息。

此外,要建立WebSocket连接,需要浏览器和服务器握手进行建立连接。一旦连接建立,WebSocket可以在浏览器和服务器之间双向发送或接受信息。总的来说,WebSocket提供了一个高效、实时的双向通信方案。

package com.litblue.im.config.properties;

import com.alibaba.fastjson2.JSONObject;
import com.litblue.starter.pojo.im.properties.MsgContentProperties;

import java.util.HashMap;
import java.util.Map;

public class RequestUriUtils {

    /**
     * 将路径参数转换成Map对象,如果路径参数出现重复参数名,将以最后的参数值为准
     *
     * @param uri 传入的携带参数的路径
     * @return
     */
    public static Map<String, String> getParams(String uri) {
        Map<String, String> params = new HashMap<>(10);

        int idx = uri.indexOf("?");
        if (idx != -1) {
            String[] paramsArr = uri.substring(idx + 1).split("&");

            for (String param : paramsArr) {
                idx = param.indexOf("=");
                params.put(param.substring(0, idx), param.substring(idx + 1));
            }
        }

        return params;
    }

    /**
     * 获取URI中参数以外部分路径
     *
     * @param uri
     * @return
     */
    public static String getBasePath(String uri) {
        if (uri == null || uri.isEmpty())
            return null;

        int idx = uri.indexOf("?");
        if (idx == -1)
            return uri;

        return uri.substring(0, idx);
    }

    /**
     * 解析消息内容
     *
     * @return
     */
    public static MsgContentProperties handleMessageInvoke(String params) {
        MsgContentProperties msgContentProperties = JSONObject.parseObject(params, MsgContentProperties.class);
        return msgContentProperties;
    }

}

web配置类

package com.litblue.im.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "chat.websocket")
public class WebSocketProperties {

  private Integer port = 10001; // 监听端口
  private String path = "/ws"; // 请求路径
  private Integer boss = 2; // bossGroup线程数
  private Integer work = 2; // workGroup线程数

}
package com.litblue.im.config.netty;

import com.litblue.im.config.properties.WebSocketProperties;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NioWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

  @Autowired
  private WebSocketProperties webSocketProperties;
  @Autowired
  private NioWebSocketHandler nioWebSocketHandler;

  @Override
  protected void initChannel(SocketChannel socketChannel) {
    socketChannel.pipeline()
        .addLast(new HttpServerCodec())
        .addLast(new ChunkedWriteHandler())
        .addLast(new HttpObjectAggregator(8192))
        .addLast(nioWebSocketHandler)
        .addLast(new WebSocketServerProtocolHandler(webSocketProperties.getPath(), null, true, 65536));
  }
}
package com.litblue.im.config.netty;

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class NioWebSocketChannelPool {

  private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

  /**
   * 新增一个客户端通道
   *
   * @param channel
   */
  public void addChannel(Channel channel) {
    channels.add(channel);
  }

  /**
   * 移除一个客户端连接通道
   *
   * @param channel
   */
  public void removeChannel(Channel channel) {
    channels.remove(channel);
  }

  /**
   * 获取所有连接信息
   * @return
   */
  public ChannelGroup getAllChannelGroup() {
    return channels;
  }

}
package com.litblue.im.config.netty;

import com.litblue.im.config.properties.WebSocketProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class NioWebSocketServer implements InitializingBean, DisposableBean {

  @Autowired
  private WebSocketProperties webSocketProperties;
  @Autowired
  private NioWebSocketChannelInitializer webSocketChannelInitializer;

  private EventLoopGroup bossGroup;
  private EventLoopGroup workGroup;
  private ChannelFuture channelFuture;

  @Override
  public void afterPropertiesSet() throws Exception {
    try {
      bossGroup = new NioEventLoopGroup(webSocketProperties.getBoss());
      workGroup = new NioEventLoopGroup(webSocketProperties.getWork());

      ServerBootstrap serverBootstrap = new ServerBootstrap();
      serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)
          .group(bossGroup, workGroup)
          .channel(NioServerSocketChannel.class)
          .localAddress(webSocketProperties.getPort())
          .childHandler(webSocketChannelInitializer);

      channelFuture = serverBootstrap.bind().sync();
    } finally {
      if (channelFuture != null && channelFuture.isSuccess()) {
        log.info("Netty server startup on port: {} (websocket) with context path '{}'", webSocketProperties.getPort(), webSocketProperties.getPath());
      } else {
        log.error("Netty server startup failed.");
        if (bossGroup != null)
          bossGroup.shutdownGracefully().sync();
        if (workGroup != null)
          workGroup.shutdownGracefully().sync();
      }
    }
  }

  @Override
  public void destroy() throws Exception {
    log.info("Shutting down Netty server...");
    if (bossGroup != null)
      bossGroup.shutdownGracefully().sync();
    if (workGroup != null)
      workGroup.shutdownGracefully().sync();
    if (channelFuture != null)
      channelFuture.channel().closeFuture().syncUninterruptibly();
    log.info("Netty server shutdown.");
  }
}
package com.litblue.im.config.netty;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.litblue.api.client.GetUserInfoClient;
import com.litblue.common.exception.UnauthorizedException;
import com.litblue.common.utils.CollUtils;
import com.litblue.im.config.properties.RequestUriUtils;
import com.litblue.im.config.properties.WebSocketProperties;
import com.litblue.im.service.IMsgContentService;
import com.litblue.starter.cache.redis.RedisCache;
import com.litblue.starter.cache.redis.RedisKeys;
import com.litblue.starter.pojo.im.domain.MsgContent;
import com.litblue.starter.pojo.im.properties.MsgContentProperties;
import com.litblue.starter.pojo.user.domian.LitUserInfo;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.AttributeKey;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * 通讯处理核心类,主要作用处理wss消息
 */
@Slf4j
@ChannelHandler.Sharable
@Component
public class NioWebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    @Autowired
    private NioWebSocketChannelPool webSocketChannelPool;
    @Autowired
    private WebSocketProperties webSocketProperties;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private IMsgContentService msgContentService;


    /**
     * 建立连接
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.debug("客户端连接:{}", ctx.channel().id());
        webSocketChannelPool.addChannel(ctx.channel());
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.debug("客户端断开连接:{}", ctx.channel().id());
        webSocketChannelPool.removeChannel(ctx.channel());
        super.channelInactive(ctx);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.channel().flush();
    }

    /**
     * 收到消息进行处理
     *
     * @param ctx
     * @param frame
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 根据请求数据类型进行分发处理
        if (frame instanceof PingWebSocketFrame) {
            pingWebSocketFrameHandler(ctx, (PingWebSocketFrame) frame);
        } else if (frame instanceof TextWebSocketFrame) {
            textWebSocketFrameHandler(ctx, (TextWebSocketFrame) frame);
        } else if (frame instanceof CloseWebSocketFrame) {
            closeWebSocketFrameHandler(ctx, (CloseWebSocketFrame) frame);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("客户端请求数据类型:{}", msg.getClass());
        if (msg instanceof FullHttpRequest) {
            fullHttpRequestHandler(ctx, (FullHttpRequest) msg);
        }
        super.channelRead(ctx, msg);
    }

    /**
     * 处理连接请求,客户端WebSocket发送握手包时会执行这一次请求
     *
     * @param ctx
     * @param request
     */
    private void fullHttpRequestHandler(ChannelHandlerContext ctx, FullHttpRequest request) {
        String uri = request.uri();
        Map<String, String> params = RequestUriUtils.getParams(uri);
        log.debug("客户端请求参数:{}", params);
        String token = params.get("token").toString();
        log.info("token:{}", token);
        Long userId = redisCache.getCacheObject(RedisKeys.DEFINE_TOKEN + token);
        if (userId == null) {
            throw new UnauthorizedException("尚未完成登录");
        }
        log.info("userId==>,{}", userId);
        String route = params.get("route").toString();
        if (params.containsKey("ack") && "true".equals(params.get("ack").toString())) {
            // 消息已读确认
            String targetId = params.get("targetId").toString();
            route += targetId;
            msgContentService.handleFinishMessageReading(userId.toString(), targetId);
        }
        //绑定连接用户
        AttributeKey<String> userKey = AttributeKey.valueOf("user");
        ctx.channel().attr(userKey).set(userId.toString());
        //绑定连接的路由
        AttributeKey<String> routeKey = AttributeKey.valueOf("route");
        ctx.channel().attr(routeKey).set(route);
        // 判断请求路径是否跟配置中的一致
        if (webSocketProperties.getPath().equals(RequestUriUtils.getBasePath(uri)))
            // 因为有可能携带了参数,导致客户端一直无法返回握手包,因此在校验通过后,重置请求路径
            request.setUri(webSocketProperties.getPath());
        else
            ctx.close();
    }

    /**
     * 客户端发送断开请求处理
     *
     * @param ctx
     * @param frame
     */
    private void closeWebSocketFrameHandler(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
        webSocketChannelPool.removeChannel(ctx.channel());
        try {
            channelInactive(ctx);
            ctx.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 创建连接之后,客户端发送的消息都会在这里处理
     *
     * @param ctx
     * @param frame
     */
    private void textWebSocketFrameHandler(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
        String userId = ctx.channel().attr(AttributeKey.valueOf("user")).get().toString();
        log.info("当前登录的用户是:{}", userId);
        String text = frame.text();
        MsgContentProperties msgContentProperties = JSON.parseObject(text, MsgContentProperties.class);
        //设置发送时间
        msgContentProperties.setSendTime(new Date());
        //聊天对话分享
        if ("0".equals(msgContentProperties.getMsgType()) || "2".equals(msgContentProperties.getMsgType())) {
            handleWebCommonMessage(ctx, msgContentProperties);
        } else if ("1".equals(msgContentProperties.getMsgType())) {
            //文件分享
            handleWebFileMessage(ctx, msgContentProperties);
        } else if ("3".equals(msgContentProperties.getMsgType())) {
            //图片分享
            handleFileImageMessage(ctx,msgContentProperties);
        } else if ("4".equals(msgContentProperties.getMsgType())) {
            //视频通话邀约
            handleInviteVideoCallMessage(ctx, msgContentProperties);
        } else if ("5".equals(msgContentProperties.getMsgType())) {
            //同意接收视频对话,通知加入ice
            handleAgreeVideoCallMessage(msgContentProperties);
        } else if ("6".equals(msgContentProperties.getMsgType())) {
            //通话开始,交换信令,offer,answer,candidate
            handleVideoCallMessage(msgContentProperties);
        } else {

        }
    }

    /**
     * 发送图片资源
     * @param ctx
     * @param msgContentProperties
     */
    private void handleFileImageMessage(ChannelHandlerContext ctx, MsgContentProperties msgContentProperties) {
        this.handleWebFileMessage(ctx,msgContentProperties);
    }

    /**
     * 处理文件分享
     *
     * @param ctx
     * @param msgContentProperties
     */
    private void handleWebFileMessage(ChannelHandlerContext ctx, MsgContentProperties msgContentProperties) {
        List<String> acceptUserId = msgContentProperties.getAcceptUserId();
        List<Channel> channelList = findChannelByUserId(acceptUserId);
        AtomicReference<Boolean> isLook = new AtomicReference<>(false);
        channelList.stream().forEach(channel -> {
            this.updateMessageReadStatus(channel, msgContentProperties, isLook);
        });
        // 处理消息心跳并返回给自己
        handleAndSendHeartbeat(ctx, msgContentProperties, isLook.get());
        //保存聊天记录
        saveChatRecords(acceptUserId, msgContentProperties);
    }

    /**
     * 同意通话
     *
     * @param msgContentProperties
     */
    private void handleAgreeVideoCallMessage(MsgContentProperties msgContentProperties) {
        List<String> acceptUserId = msgContentProperties.getAcceptUserId();
        List<Channel> channelList = findChannelByUserId(acceptUserId);
        Channel channel = channelList.get(0);
        channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(msgContentProperties)));
    }

    /**
     * 通话邀约
     *
     * @param ctx
     * @param msgContentProperties
     */
    private void handleInviteVideoCallMessage(ChannelHandlerContext ctx, MsgContentProperties msgContentProperties) {
        List<String> acceptUserId = msgContentProperties.getAcceptUserId();
        Map<String, Object> messageMap = msgContentProperties.getMessageMap();
        //设置来电铃声
        messageMap.put("callMusic", "http://101.43.99.167:9000/blue-oss/r8nx5-0hhz7.mp3");
        // 计算来电过期时间,当前时间加一分钟
        Instant now = Instant.now();
        Instant overdueTime = now.plus(Duration.ofMinutes(1));
        messageMap.put("overdueTime", overdueTime.toString());  // 转换为字符串表示

        List<Channel> channelList = findChannelByUserId(acceptUserId);
        if (CollectionUtils.isEmpty(channelList)) {
            //对方不在线
            msgContentProperties.setIsRead(false);
            msgContentProperties.setReadName("未读");
            msgContentProperties.setMsgContent("未接来电");
        } else {
            Channel channel = channelList.get(0);
            channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(msgContentProperties)));
        }
        saveChatRecords(acceptUserId, msgContentProperties);
    }

    /**
     * 处理普通对话消息
     *
     * @param ctx
     * @param msgContentProperties
     */
    private void handleWebCommonMessage(ChannelHandlerContext ctx, MsgContentProperties msgContentProperties) {
        // 接收人id
        List<String> acceptUserId = msgContentProperties.getAcceptUserId();
        AtomicReference<Boolean> isLook = new AtomicReference<>(false);
        List<Channel> channelList = findChannelByUserId(acceptUserId);
        //对方离线
        if (CollectionUtils.isEmpty(channelList)) {
            //消息未读或者不在对话路由下面
            msgContentProperties.setIsRead(false);
            msgContentProperties.setReadName("未读");
        }
        channelList.stream().forEach(channel -> {
            this.updateMessageReadStatus(channel, msgContentProperties, isLook);
        });
        // 处理消息心跳并返回给自己
        handleAndSendHeartbeat(ctx, msgContentProperties, isLook.get());
        //保存聊天记录
        saveChatRecords(acceptUserId, msgContentProperties);
    }

    /**
     * 处理并返回消息心跳
     *
     * @param ctx                  ChannelHandlerContext 上下文
     * @param msgContentProperties 消息内容属性
     * @param isLook               是否已读的状态
     */
    private void handleAndSendHeartbeat(ChannelHandlerContext ctx, MsgContentProperties msgContentProperties, boolean isLook) {
        // 创建新的消息属性对象并设置读取状态
        MsgContentProperties properties = BeanUtil.copyProperties(msgContentProperties, MsgContentProperties.class);
        properties.setIsRead(isLook);
        properties.setReadName(isLook ? "已读" : "未读");
        // 返回给自己心跳消息
        ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(properties)));
    }

    /**
     * 更新消息的已读/未读状态
     *
     * @param channel              当前处理的Channel
     * @param msgContentProperties 消息内容属性
     * @param isLook               是否已读的原子引用
     */
    private void updateMessageReadStatus(Channel channel, MsgContentProperties msgContentProperties, AtomicReference<Boolean> isLook) {
        String route = channel.attr(AttributeKey.valueOf("route")).get().toString();
        String chatRoute = "chat" + msgContentProperties.getSendUserId();
        if (chatRoute.equals(route)) {
            msgContentProperties.setIsRead(true);
            msgContentProperties.setReadName("已读");
            isLook.set(true);
            channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(msgContentProperties)));
        } else {
            msgContentProperties.setIsRead(false);
            msgContentProperties.setReadName("未读");
            isLook.set(false);
        }
    }


    /**
     * 处理rtc视频通讯
     *
     * @param msgContentProperties
     */
    private void handleVideoCallMessage(MsgContentProperties msgContentProperties) {
        String message = JSONObject.toJSONString(msgContentProperties);
        sendToUser(msgContentProperties.getAcceptUserId(), message);
        String sendUserId = msgContentProperties.getSendUserId();
        List<String> sendUserIdList = Collections.singletonList(sendUserId);
        sendToUser(sendUserIdList, message);
    }

    private void sendToUser(List<String> userId, String message) {
        // 找到目标用户的 Channel 并发送消息
        List<Channel> targetChannel = findChannelByUserId(userId);
        if (!CollectionUtils.isEmpty(targetChannel)) {
            targetChannel.stream().forEach(channel -> {
              /*  String route = channel.attr(AttributeKey.valueOf("route")).get().toString();
                if ("rtc".equals(route)) {
                    channel.writeAndFlush(new TextWebSocketFrame(message));
                }*/
                channel.writeAndFlush(new TextWebSocketFrame(message));

            });
        } else {
            log.warn("用户 {} 的 Channel 未找到", userId);
        }
    }

    /**
     * 查找连接池对应Channel
     *
     * @param userId
     * @return
     */
    public List<Channel> findChannelByUserId(List<String> userId) {
        ChannelGroup channels = webSocketChannelPool.getAllChannelGroup();
        if (CollectionUtils.isEmpty(channels)) {
            return CollUtils.emptyList();
        }
        List<Channel> channelList = channels.stream().filter(item -> {
            return userId.contains(item.attr(AttributeKey.valueOf("user")).get().toString());
        }).collect(Collectors.toList());
        return channelList;
    }

    /**
     * 处理客户端心跳包
     *
     * @param ctx
     * @param frame
     */
    private void pingWebSocketFrameHandler(ChannelHandlerContext ctx, PingWebSocketFrame frame) {
        ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
    }

    /**
     * 保存聊天记录
     *
     * @param acceptUserId
     * @param msgContentProperties
     */
    private void saveChatRecords(List<String> acceptUserId, MsgContentProperties msgContentProperties) {
        List<MsgContent> msgContents = acceptUserId.stream()
                .map(userId -> {
                    MsgContent msgContent = BeanUtil.copyProperties(msgContentProperties, MsgContent.class);
                    String messageMap = JSONObject.toJSONString(msgContentProperties.getMessageMap());
                    msgContent.setMessageMap(messageMap);
                    msgContent.setSendUserIsRemove(false);
                    msgContent.setAcceptUserIsRemove(false);
                    msgContent.setAcceptUserId(String.valueOf(userId)); // Assuming acceptUserId is a String in MsgContent
                    return msgContent;
                })
                .collect(Collectors.toList());
        this.msgContentService.saveMsgContents(msgContents); // Ensure this method saves a list of MsgContent objects
    }
}

聊天记录持久化到mongodb

 /**
     * 保存聊天记录
     *
     * @param acceptUserId
     * @param msgContentProperties
     */
    private void saveChatRecords(List<String> acceptUserId, MsgContentProperties msgContentProperties) {
        List<MsgContent> msgContents = acceptUserId.stream()
                .map(userId -> {
                    MsgContent msgContent = BeanUtil.copyProperties(msgContentProperties, MsgContent.class);
                    String messageMap = JSONObject.toJSONString(msgContentProperties.getMessageMap());
                    msgContent.setMessageMap(messageMap);
                    msgContent.setSendUserIsRemove(false);
                    msgContent.setAcceptUserIsRemove(false);
                    msgContent.setAcceptUserId(String.valueOf(userId)); // Assuming acceptUserId is a String in MsgContent
                    return msgContent;
                })
                .collect(Collectors.toList());
        this.msgContentService.saveMsgContents(msgContents); // Ensure this method saves a list of MsgContent objects
    }
package com.litblue.starter.pojo.im.properties;

import com.litblue.starter.pojo.artwork.domain.LitArtworkInfo;
import com.litblue.starter.pojo.artwork.vo.LitArtworkInfoVo;
import lombok.Data;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 消息内容
 */
@Data
public class MsgContentProperties {

    /**
     * 发送类型 (0 群发 1 单发)
     */
    private String sendType;

    /**
     * 消息类型 (0 文本对话
     *          1 文件
     *          2 作品分享
     *          3 发送图片
     *          4 发起视频对话请求
     *          5 接收视频对话
     *          6 拒绝视频对话)
     */
    private String msgType;

    /**
     * 其他业务消息结构体
     */
    private Map<String,Object> messageMap = new HashMap<>();

    /**
     * 消息内容
     */
    private String msgContent;


    /**
     * 发送人id
     */
    private String sendUserId;

    /**
     * 接收人id(在两人对话聊天,或者分享的时候携带)
     */
    private List<String> acceptUserId;

    /**
     * 群组id(在群发的时候携带)
     */
    private String groupId;

    /**
     * 发送时间
     */
    private Date sendTime;


    /**
     * 消息是否已读(0已读 1未读)
     */
    private Boolean isRead;

    /**
     * 消息状态名称
     */
    private String readName;



}
   @Override
    public void saveMsgContents(List<MsgContent> msgContents) {
        msgContentRepository.saveAll(msgContents);
    }

 

posted @ 2024-08-24 22:44  橘子味芬达水  阅读(69)  评论(0编辑  收藏  举报