首页 > 其他分享 >netty实现私信聊天

netty实现私信聊天

时间:2024-08-24 22:48:56浏览次数:10  
标签:私信 netty msgContentProperties ctx private 聊天 import param channel

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);
    }

 

标签:私信,netty,msgContentProperties,ctx,private,聊天,import,param,channel
From: https://www.cnblogs.com/azwz/p/18378406

相关文章

  • 集群聊天服务器
    集群聊天服务器项目地址:Focuspresent/ChatServer(github.com)环境搭建(基于Unbuntu20.04)boost库安装sudoaptinstalllibboost-all-devmysql开发库安装sudoaptinstalllibmysqlclient-devmuduo库安装先需要安装cmake以及boost库sudoaptinstallcmake下载安......
  • 将洛谷私信接入Windows
    首先下载一个私信Github:https://github.com/GCSG01/LG_Show_Massger/archive/refs/heads/main.zip然后解压,找到src/settings.json,把你的洛谷cookie和UID填进去,点击Start.cmd运行。(其余的不要改)之后不出意外就会有两个窗口:AI功能:下载仓库:https://github.com/OI-liyi......
  • 聊天器人时代:AI智能客服助力企业转型升级
    近年来,随着人工智能技术的不断发展,越来越多的企业开始采用智能客服系统来提升客户服务质量和效率。这种新型的客服模式不仅可以为企业节省人成本,还可以实现24小时全天候在线服务,为客户提供更加便捷、快捷的咨询和解决方案AI智能客服系统主要通过聊天器人技术实现。聊天器人是一......
  • AI聊天機器人全解析:類型、應用場景與四大優勢
    在数字化浪潮的推动下,人工智能(AI)正逐渐渗透到我们生活的方方面面。其中,AI聊天機器人作为一种新兴的交互方式,正在重塑我们与机器沟通的模式。本文将深入解析AI聊天機器人的基础概念、主要类型、应用场景以及它们所带来的四大优势。聊天機器人:虚拟世界中的智能助手AI聊天機器人......
  • 漂流瓶聊天平台全自动挂机玩法
    项目介绍:窃语漂流瓶挂机项目也是一直存在的项目,语聊也是一个经久不衰的领域我们汇聚了最新的一套玩法和软件大大提高收益和降低防控的出现。挂机项目和矩阵多开项目优势:注册女号不要实名可多号提现解决账号渠道低成本无限注册解决ai聊天矩阵问题最新助手对接智......
  • 2024年十大聊天机器人构建平台
    聊天机器人现在在客户服务和业务自动化中扮演着至关重要的角色,这项技术允许企业与客户快速实时地互动。用于创建聊天机器人的技术正在迅速发展,这对聊天机器人构建工具产生了直接影响。一些领先者中的先驱者在2024年正在推出具有革命性特点和界面的产品。本文介绍了顶级聊天机器......
  • Chainlit接入FastGpt接口快速实现自定义用户聊天界面
    前言由于fastgpt只提供了一个分享用的网页应用,网页访问地址没法自定义,虽然可以接入NextWeb/ChatGPTweb等开源应用。但是如果我们想直接给客户应用,还需要客户去设置配置,里面还有很多我们不想展示给客户的东西怎么办?于是,我使用Chainlit实现了一个无缝快速接入fastgpt实现自......
  • Netty 异步任务模型 及 Future-Listener 机制
    https://cloud.tencent.com/developer/article/2246990一、Netty模型二、异步模型三、Future-Listener机制四、Future-Listener机制代码示例 以服务器端为例1.线程池:Netty模型核心就是两个线程池,BossGroup线程池和WorkerGroup线程池;①BossGroup......
  • 一款专为内网办公环境设计的操作系统,集成了Word、Excel、PPT、PDF编辑器,内网聊天、白
    前言在当今数字化办公时代,企业面临着多样化的办公需求。现有软件往往存在一些痛点,如操作复杂、兼容性差、资源消耗高,以及在内网环境下的通讯和文件共享不便。这些限制不仅影响了工作效率,也制约了企业的数字化转型。因此,一款能够处理这些问题的软件显得尤为迫切。介绍GodoOS......
  • 隐秘之舞:探索聊天记录隐藏与密友构建的艺术境界
    在数字时代的洪流中,社交的经纬线编织出一幅幅错综复杂的人际图谱。在这片由0与1构建的虚拟世界里,聊天记录与密友关系,成为了我们情感交流、思想碰撞的重要载体。然而,随着信息量的激增与个人隐私需求的日益增强,隐藏聊天记录与构建密友圈,逐渐演变成了一门精致而深邃的艺术,它们不仅......