从零用java实现 小红书 springboot vue uniapp (7)im 在线聊天功能 关注功能

前言

移动端演示 http://8.146.211.120:8081/#/

前面的文章我们主要完成了笔记的点赞和收藏及留言功能 今天我们讲解点赞关注 im 聊天功能
关注

我们需要有一个关注的操作 这里我们复用个人中心页面

按钮会有三种形式 关注 取消关注 互相关注三种样式

bash 复制代码
			<view class="gui-flex gui-align-items-center gui-justify-content-center" >
							<button
									v-show="author.isFollow"
									@tap="cancelfollowAuthor"
									type="default"
									class="gui-button-mini xhs-border-radius50  xhs-border-white"
									style="width:150rpx;margin-right: 20rpx;background: transparent;backdrop-filter: blur(10px);background-color: rgba(255, 255, 255, 0.1);">
								<text class="gui-color-white  gui-icons">取消关注</text>
							</button>

							<button
									v-show="!author.isFollow&&author.isFollowMe"
									@tap="followAuthor"
									type="default"
									class="gui-button-mini xhs-border-radius50 "
									style="width:150rpx;margin-right: 20rpx;background: transparent;backdrop-filter: blur(10px);background-color: #FF3749;">
								<text class="gui-color-white  gui-icons">回关</text>
							</button>
							<button
									v-show="!author.isFollow&&!author.isFollowMe"
									@tap="followAuthor"
									type="default"
									class="gui-button-mini xhs-border-radius50 "
									style="width:150rpx;margin-right: 20rpx;background: transparent;backdrop-filter: blur(10px);background-color: #FF3749;">
								<text class="gui-color-white  gui-icons">关注</text>
							</button>

关注和点赞功能实现原理大致相同 只不过有一个互相关注

后台先创建一个关注表

bash 复制代码
CREATE TABLE `business_follow` (
  `ID` varchar(32) NOT NULL,
  `AUTHOR_ID` varchar(32) DEFAULT NULL COMMENT '被关注id',
  `AUTHOR_NAME` varchar(255) DEFAULT NULL COMMENT '被关注名字',
  `FOLLOW_ID` varchar(32) DEFAULT NULL COMMENT '关注者',
  `FOLLOW_NAME` varchar(255) DEFAULT NULL COMMENT '关注者名字',
  `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关注表';

关注人id 被关注人id 关注时间 有这三个就能基本实现

点击关注 插入 authorId对方id followId为当前登录人id

但是此时要注意

当你查询粉丝列表时 select xx from business_follow where AUTHOR_ID = #{当前登录人}

这样还不行 因为只查询到了粉丝列表

所以需要加一个子查询 我是否关注了对方 这样才能实现互关效果

bash 复制代码
<!--    查询关注我的所有列表信息 并在查询的时候设置子查询 查询当前用户作为关注者的时候有没有关注对方-->
    <select id="selectFollowMeList" resultType="com.dd.admin.business.follow.domain.FollowVo">
        SELECT
            b.AUTHOR_ID,
            b.AUTHOR_NAME,
            b.AVATAR_URL,
            b.DESCRIPTION,
            (
                SELECT
                    count(1)
                FROM
                    business_follow
                WHERE
                    follow_id = #{targetId}
                AND author_id = b.AUTHOR_ID
            ) AS isFollow
            ,
            (
                SELECT
                    count(1)
                FROM
                    business_follow
                WHERE
                    follow_id = b.AUTHOR_ID
                AND author_id = #{targetId}
            ) AS isFollowMe
        FROM
            business_follow a
        LEFT JOIN business_author b ON a.follow_id = b.author_id
        WHERE
            1 = 1
        AND a.AUTHOR_ID = #{authorId}
        ORDER BY
            a.create_time DESC
    </select>

有了用户关系 下一步就可以进行聊天了

这是我的聊天记录列表 由于没有通讯录

此列表查询的是所有跟你聊过天的人的列表

下面我们从后台 搭建im 服务器 这里我使用的是tio

pom 加上tio的依赖

bash 复制代码
        <dependency>
            <groupId>org.t-io</groupId>
            <artifactId>tio-websocket-spring-boot-starter</artifactId>
            <!--此版本号跟着tio主版本号一致即可-->
            <version>3.3.2.v20190601-RELEASE</version>
        </dependency>

配置类

bash 复制代码
package com.dd.admin.business.webSocket;


import com.alibaba.fastjson.JSON;
import com.dd.admin.common.utils.AddressUtils;
import com.dd.admin.common.utils.IPUtils;
import com.dd.admin.common.utils.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.utils.lock.SetWithLock;
import org.tio.websocket.common.WsRequest;
import org.tio.websocket.server.handler.IWsMsgHandler;
import org.tio.websocket.starter.TioWebSocketServerBootstrap;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component
public class MyWebSocketMsgHandler implements IWsMsgHandler {

    MyWebSocketMsgHandler handler;

    private static Logger log = LoggerFactory.getLogger(MyWebSocketMsgHandler.class);

    @PostConstruct
    public void init() {
        handler = this;
    }

    @Autowired
    public TioWebSocketServerBootstrap bootstrap;

    @Autowired
    Map<String, MsgHandlerInterface> handlerInterfaceMap;


    @Override
    public HttpResponse handshake(HttpRequest request, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
        String authorId = request.getParam("authorId");
        String authorName = request.getParam("authorName");

        Tio.bindUser(channelContext,authorId);



        String ipAddr = request.getClientIp();
        String realAddress = AddressUtils.getRealAddress(ipAddr);

        System.out.println(authorId+":进入了Tio id:"+authorId+" ip:"+ ipAddr);

        SetWithLock<ChannelContext> channelContexts =  Tio.getAllChannelContexts(bootstrap.getServerGroupContext());
        Set<ChannelContext> contextList = channelContexts.getObj();
        System.out.println("当前在线用户:");
        for(ChannelContext context:contextList){
            System.out.println(context.userid+"\t");
        }

       Integer count = channelContexts.size();
        System.out.println(count);

        return httpResponse;
    }

    @Override
    public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
//        System.out.println("握手成功进入群组");

    }

    @Override
    public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        System.out.println("接收到bytes消息");
        return null;
    }

    @Override
    public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        return null;
    }

    @Override
    public Object onText(WsRequest wsRequest, String text, ChannelContext channelContext) throws Exception {
        if(text.equals("心跳内容")) return null;

        System.out.println("接收到文本消息:"+text);

        Map map = JSON.parseObject(text,Map.class);

        String handlerType =(String)map.get("handlerType");

        if(!StringUtil.isEmpty(handlerType)){
            MsgHandlerInterface msgHandler = (MsgHandlerInterface) handlerInterfaceMap.get(handlerType);
            if(msgHandler!=null){
                msgHandler.handler(map,channelContext);
            }else{
                log.debug("非法请求...");
            }
        }else{
            log.debug("非法请求...");
        }


        System.out.println(map);
        return null;
    }
}

处理点对点聊天的接口

bash 复制代码
package com.dd.admin.business.webSocket.handler;

import cn.hutool.core.bean.BeanUtil;
import com.dd.admin.business.chat.domain.ChatVo;
import com.dd.admin.business.chat.entity.Chat;
import com.dd.admin.business.chat.service.ChatService;
import com.dd.admin.business.webSocket.MsgHandlerInterface;
import com.dd.admin.business.webSocket.util.TioUtil;
import com.dd.admin.common.utils.AddressUtils;
import com.dd.admin.common.utils.HttpContext;
import com.dd.admin.common.utils.IPUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import org.tio.http.common.HttpRequest;
import org.tio.utils.lock.SetWithLock;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Map;
import java.util.Set;


@Component
@Slf4j
@Service("5")
public class P2PMessageHandler implements MsgHandlerInterface {

    public static P2PMessageHandler handler;

    @Autowired
    ChatService chatService;



    @Override
    public Object handler(Map map, ChannelContext context ){
        Chat chat = BeanUtil.toBean(map, Chat.class);

        chat.setIpAddress(context.getClientNode().getIp());
        chat.setIpRealAddress(AddressUtils.getRealAddress(chat.getIpAddress())); //ip真实地址
        chatService.save(chat);

        ChatVo chatVo = chatService.selectChat(chat.getChatId());

        //t-io支持多点登录,获取的是一个集合,因为此账号可能存在多个连接哦
        SetWithLock<ChannelContext> contexts = Tio.getChannelContextsByUserid(context.getGroupContext(), chat.getToId());
        //用户在线
        if(contexts!=null && contexts.size() > 0) {
            Set<ChannelContext> contextList = contexts.getObj();
            //t-io支持多点登录,获取的是一个集合,向集合发送聊天信息
            for (ChannelContext con : contextList) {
                TioUtil.sendMessage(con, "5", chatVo);
            }
        }

        //也要给我自己发用于数据回显
        //t-io支持多点登录,获取的是一个集合,因为此账号可能存在多个连接哦
        SetWithLock<ChannelContext> contexts1 = Tio.getChannelContextsByUserid(context.getGroupContext(), chat.getFromId());
        //用户在线
        if(contexts1!=null && contexts1.size() > 0) {
            Set<ChannelContext> contextList = contexts1.getObj();
            //t-io支持多点登录,获取的是一个集合,向集合发送聊天信息
            for (ChannelContext con : contextList) {
                TioUtil.sendMessage(con, "5", chatVo);
            }
        }
        return null;
    }}

yml端口配置

bash 复制代码
tio:
  websocket:
    server:
      port: 9326
      heartbeat-timeout: 6000
    # 集群配置 默认关闭
    cluster:
      enabled: false
      # 集群是通过redis的Pub/Sub实现,所以需要配置Redis
      redis:
        ip: 127.0.0.1
        port: 6379
      all: true
      group: true
      ip: true
      user: true

最后在启动类 配置启动即可

这样我们的后台服务就搭建好了

前台怎么跟他 进行交互呢?

我们用apipost的websocket连接测试一下

后台监听到了连接 但是过了一会就断了 因为我们设置的有心跳连接

所以我们封装了一个uniapp 可用的 连接类 实现了心跳连接

bash 复制代码
import GraceRequestConfig from '@/custom/graceRequestConfig.js';


// 判断socket是否已经连接成功
var socketOpen = false;

var socketUserClose = false;  //主动调用  关闭后不在启动
// socket是否已经调用关闭function //自然关闭
var socketClose = false;
// socket发送的消息队列
var socketMsgQueue = [];
// 判断心跳变量
var heart = '';
// 心跳失败次数
var heartBeatFailCount = 0;
// 终止心跳
var heartBeatTimeOut = null;
// 终止重新连接
var connectSocketTimeOut = null;

var user = uni.getStorageSync('user')

// 定义WebSocket相关功能的对象
const webSocket = {
	/**
	 * 创建一个 WebSocket 连接
	 * @param {options}
	 *   url      String    是    开发者服务器接口地址,必须是 wss 协议,且域名必须是后台配置的合法域名
	 *   header    Object    否    HTTP Header, header 中不能设置 Referer
	 *   method    String    否    默认是GET,有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
	 *   protocols  StringArray    否    子协议数组    1.4.0
	 *   success    Function    否    接口调用成功的回调函数
	 *   fail      Function    否    接口调用失败的回调函数
	 *   complete    Function    否    接口调用结束的回调函数(调用成功、失败都会执行)
	 */
	connectSocket: function (options) {
		socketClose = false;
		socketUserClose = false;
		socketMsgQueue = [];
		const wsUrl = GraceRequestConfig.wsUrl + '?authorName=' + user.authorName + "&authorId=" + user.authorId;
		console.log('开始连接')
		console.log('heartBeatTimeOut')
		console.log(heartBeatTimeOut)
		console.log('socketUserClose')
		console.log(socketUserClose)
		if(!socketUserClose){
			//重新心跳
			webSocket.startHeartBeat();
		}
		if(socketOpen){
			console.log('已连接ws不在重复连接')
		}

		// const wsUrl = 'ws://192.168.10.98:9326?authorName=' + user.authorName + "&authorId=" + user.authorId;
		uni.connectSocket({
			url: wsUrl,
			success: function (res) {
				if (options) {
					// 成功回调
					options.success && options.success(res);
				}
			},
			fail: function (res) {
				if (options) {
					// 失败回调
					options.fail && options.fail(res);
				}
			}
		});
	},

	/**
	 * 通过 WebSocket 连接发送数据
	 * @param {options}
	 *   data    String / ArrayBuffer    是    需要发送的内容
	 *   success    Function    否    接口调用成功的回调函数
	 *   fail    Function    否    接口调用失败的回调函数
	 *   complete    Function    否    接口调用结束的回调函数(调用成功、失败都会执行)
	 */
	sendSocketMessage: function (options) {
		// console.log('发送消息')
		if (socketOpen) {
			uni.sendSocketMessage({
				data: options.msg,
				success: function (res) {
					if (options) {
						options.success && options.success(res);
					}
				},
				fail: function (res) {
					if (options) {
						options.fail && options.fail(res);
					}
				}
			});
		} else {
			// app.msg('聊天服务器已断开...')
			// socketMsgQueue.push(options.msg);
		}
	},

	/**
	 * 关闭 WebSocket 连接。
	 * @param {options}
	 *   code    Number    否    一个数字值表示关闭连接的状态号,表示连接被关闭的原因。如果这个参数没有被指定,默认的取值是1000 (表示正常连接关闭)
	 *   reason    String    否    一个可读的字符串,表示连接被关闭的原因。这个字符串必须是不长于123字节的UTF-8 文本(不是字符)
	 *   fail    Function    否    接口调用失败的回调函数
	 *   complete    Function    否    接口调用结束的回调函数(调用成功、失败都会执行)
	 */
	closeSocket: function (options) {
		//关闭重连定时器
		if (connectSocketTimeOut) {
			clearTimeout(connectSocketTimeOut);
			connectSocketTimeOut = null;
		}
		socketOpen = false;
		//主动调用关闭
		socketUserClose = true;
		socketClose = true;
		const self = this;
		//关闭心跳了
		self.stopHeartBeat();
		uni.closeSocket({
			success: function (res) {
				console.log('WebSocket 已关闭!');
				if (options) {
					options.success && options.success(res);
				}
			},
			fail: function (res) {
				if (options) {
					options.fail && options.fail(res);
				}
			}
		});
	},
	// 开始心跳
	startHeartBeat: function () {
		console.log('socket开始心跳');
		const self = this;
		heart = 'heart';
		self.heartBeat();
	},

	// 结束心跳
	stopHeartBeat: function () {
		console.log('socket结束心跳');
		const self = this;
		heart = '';
		if (heartBeatTimeOut) {
			clearTimeout(heartBeatTimeOut);
			heartBeatTimeOut = null;
		}
		if (connectSocketTimeOut) {
			clearTimeout(connectSocketTimeOut);
			connectSocketTimeOut = null;
		}
	},

	// 心跳
	heartBeat: function () {
		const self = this;
		if (!heart) {
			return;
		}
		self.sendSocketMessage({
			msg: "心跳内容",
			success: function (res) {
				// console.log('socket心跳成功');
				if (heart) {
					heartBeatTimeOut = setTimeout(() => {
						self.heartBeat();
					}, 5000);
				}
			},
			fail: function (res) {
				console.log('socket心跳失败');
				console.log(heartBeatFailCount);
				// if (heartBeatFailCount > 2) {
				// 	// 重连
				// 	self.connectSocket();
				// }
				if (heart) {
					heartBeatTimeOut = setTimeout(() => {
						self.heartBeat();
					}, 5000);
				}
				heartBeatFailCount++;

			},
		});
	},
	onSocketMessageCallback(callback) {
	},
};

// 监听WebSocket连接打开事件。callback 回调函数
uni.onSocketOpen(function (res) {
	console.log('WebSocket连接已打开!');
	// 如果已经调用过关闭function
	// 如果已经调用过关闭function
	if (socketClose) {
		console.log('不再自行关闭')
		// webSocket.closeSocket();
	} else {
		socketOpen = true;
		for (var i = 0; i < socketMsgQueue.length; i++) {
			uni.sendSocketMessage(socketMsgQueue[i]);
		}
		socketMsgQueue = [];
		webSocket.startHeartBeat();
	}
	// 发送请求离线消息状态



});

// 监听WebSocket错误。
uni.onSocketError(function (res) {
	socketOpen = false
	socketClose = true

	console.log('WebSocket连接打开失败,请检查!', res);
	console.log('异常关闭' + socketClose);
	console.log('主动关闭' + socketUserClose);
	//如果不是主动关闭的 重新链接
	if (!socketUserClose) {
		clearTimeout(connectSocketTimeOut);
		connectSocketTimeOut = setTimeout(() => {
			console.log('不是主动关闭所以重连')
			webSocket.connectSocket();
		}, 3000);
	}
});

// 监听WebSocket接受到服务器的消息事件。
uni.onSocketMessage(function (res) {
	// console.log('收到服务器内容:' + res.data)
	webSocket.onSocketMessageCallback(res)}
);

// 监听WebSocket关闭。
uni.onSocketClose(function (res) {
	console.log('WebSocket 已关闭!===================');
	socketOpen = false
	socketClose = true
	console.log('异常关闭' + socketClose);
	console.log('主动关闭' + socketUserClose);
	//如果不是主动关闭的 重新链接
	if (!socketUserClose) {
		clearTimeout(connectSocketTimeOut);
		connectSocketTimeOut = setTimeout(() => {
			//
			console.log('不是主动关闭所以重连 这里进行重连')
			webSocket.connectSocket();
		}, 3000);
	}
});

// 使用export default导出webSocket对象,方便其他模块导入使用
export default webSocket;

目前还不是特别完美 后续我会进行优化

下面就开始了 我们前后端的交互

我们在主页进行websocket连接后

输入信息发送

我们开始对格式进行分析

fromId消息由谁发的

toid 消息发送给谁

handler 类型 我们约定5为点对点聊天

messageType 0 文本聊天

发送到后台

其实这几个字段就是我们的聊天记录表字段

我们通过传到后台的这两个id 分别查询到相应的设备集合

然后进行发送即可 为什么是集合 因为 一个账号可能涉及到多端登录

数据推送到前端后

bash 复制代码
		// 滚动条滚动 [ 有新消息可以自动滚动到底部 ]
		pageScroll : function () {
			setTimeout(()=>{
				uni.pageScrollTo({
					scrollTop:999999+Math.random(),
					duration:200
				})
			},200);
		},

页面会向下继续生成 进行页面滚动即可

当页面聊天记录过多时 我们考虑分页加载聊天数据

这里和我们的笔记页表相同 下拉时 页数增加 没有数据是提示用户

bash 复制代码
getChatList(){
			app.get('/auth/getChatList', {limit:20,page:this.page,
				fromId:this.from.authorId,fromName:this.from.authorName}, '', (res => {
				//倒序查询但是正序排列 时间早的在前面
				this.chatList.unshift(... res.data.records.reverse());
				if(this.page<=res.data.pages){
					this.hasMore = true
					this.$refs.loadmorecom2.stoploadmore();
					if(this.page==1){
						setTimeout(()=>{
							this.pageScroll()
						},300)
					}
					if(this.page<res.data.pages){
						this.page++
					}else if(this.page==res.data.pages){
						this.$refs.loadmorecom2.nomore();
						uni.stopPullDownRefresh();
					}

				}else{
					this.$refs.loadmorecom2.nomore();
					this.hasMore = false
					app.msg('没有更多记录了')
					uni.stopPullDownRefresh();
				}

				this.buildData()
			}))
		},

此时因为我们的数据是倒序查询的

10 9 8 7 6 5 4 3 2 1

但是在页面展示 老的数据在前面 所以要执行数组的 .reverse() 反转方法

我们还会看到 我们做了日期的格式化

  • 小于2分钟是刚刚
  • 今天的数据显示 时分
  • 近一周的显示星期数 时分
  • 本年的显示月日 时分
  • 本年之前的 显示 年月日 时分
  • 小于两分钟的数据 创建时间只显示第一条
bash 复制代码
		buildData(){
			const records = this.chatList
			for (let i = 0; i < records.length; i++) {
				const currentRecord = records[i];
				const currentCreateTime = new Date(currentRecord.createTime);
				let j = i + 1;
				while (j < records.length) {
					const nextRecord = records[j];
					const nextCreateTime = new Date(nextRecord.createTime);
					const timeDiff = (nextCreateTime - currentCreateTime) / 1000 / 60; // 计算时间差,单位换算为分钟
					if (timeDiff <= 2) {
						records[j].createTime = null;
						j++;
					} else {
						break;
					}
				}
			}
			console.log(records)
			this.chatList = records;
		},
bash 复制代码
	messageFormatDate : function (dateStr) {
		if (!dateStr) {
			return '';
		}
		const inputDate = new Date(dateStr);
		const now = new Date();
		const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
		const oneHour = 60 * 60 * 1000; // 一小时的毫秒数
		const oneMinute = 60 * 1000;
		const twoMinutes = 2 * oneMinute;
		const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
		const weekStart = new Date(now.getTime() - 7 * oneDay);

		const diff = now - inputDate;
		if (diff < twoMinutes) {
			return "刚刚";
		} else if (inputDate >= todayStart) {
			const hours = inputDate.getHours().toString().padStart(2, '0');
			const minutes = inputDate.getMinutes().toString().padStart(2, '0');
			return `${hours}:${minutes}`;
		} else if (inputDate >= weekStart) {
			const weekDay = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
			const dayIndex = inputDate.getDay();
			const hours = inputDate.getHours().toString().padStart(2, '0');
			const minutes = inputDate.getMinutes().toString().padStart(2, '0');
			return `${weekDay[dayIndex]} ${hours}:${minutes}`;
		} else if (inputDate.getFullYear() === now.getFullYear()) {
			const month = (inputDate.getMonth() + 1).toString().padStart(2, '0');
			const day = inputDate.getDate().toString().padStart(2, '0');
			const hours = inputDate.getHours().toString().padStart(2, '0');
			const minutes = inputDate.getMinutes().toString().padStart(2, '0');
			return `${month}-${day} ${hours}:${minutes}`;
		} else {
			const year = inputDate.getFullYear();
			const month = (inputDate.getMonth() + 1).toString().padStart(2, '0');
			const day = inputDate.getDate().toString().padStart(2, '0');
			const hours = inputDate.getHours().toString().padStart(2, '0');
			const minutes = inputDate.getMinutes().toString().padStart(2, '0');
			return `${year}-${month}-${day} ${hours}:${minutes}`;
		}
	},

处理完数据后 我们再回头处理我们的聊天列表

对角标也进行了处理

针对这个聊天列表 需要注意的查询条件是

  • 首先要查询所有我收到的数据 针对这个条件 加一个子查询我发送到对方的数据(没有回复的情况)
  • 子查询加上最后一条记录 (无论双方谁发的)
  • 针对这个人我的未读数量
  • 需要按form_id进行分组
bash 复制代码
    <select id="selectChatList" resultType="com.dd.admin.business.chat.domain.ChatVo"
            parameterType="java.lang.String">
        select * from (
            SELECT
                a.FROM_ID AS authorId,
                a.FROM_NAME AS authorName,
                b.AVATAR_URL AS authorAvatar,
                a.content,
                a.create_time,
                (
                    SELECT
                        count(1)
                    FROM
                        business_chat ca
                    WHERE
                        ca.FROM_ID = a.FROM_ID
                    AND ca.to_id = #{authorId}
                    and ca.MESSAGE_STATUS = 0
                ) as unReadCount
            FROM
                business_chat a
                    LEFT JOIN
                business_author b ON a.FROM_ID = b.AUTHOR_ID
            WHERE
                a.TO_ID = #{authorId}
            UNION ALL
            SELECT
                a.TO_ID AS authorId,
                a.TO_NAME AS authorName,
                b.AVATAR_URL AS authorAvatar,
                a.content,
                a.create_time,
                0 as unReadCount
            FROM
                business_chat a
                    LEFT JOIN
                business_author b ON a.TO_ID = b.AUTHOR_ID
            WHERE
                a.FROM_ID = #{authorId}
                ORDER BY
                create_time DESC
            ) a1
            GROUP BY a1.authorId
              ORDER BY
                create_time DESC
    </select>

这个sql 稍微优点难度可以分开的进行处理

小红点的处理方法(这里我们只是粗略的实现了)

  1. 首先写一个查询未读消息的接口
  2. 当页面加载时请求接口
  3. 当收到websocket推送消息时 重新请求查询未读消息的接口
  4. 当点击某个人的聊天页面是 设置该用户的所有消息已读
  5. 此方法不能写到App.vue会提示无法设置tabbar数量
bash 复制代码
		onShow(){
			this.onShowStatus = false
			console.log('onshow')
			if(this.isLoad){
				this.getMessageList()
			}
			this.$nextTick(()=> {
				// 监听WebSocket接受到服务器的消息事件。
				websocket.onSocketMessageCallback = (res) => {
					console.log('我是message页面收到消息的提示')
					app.setMessageTabBarBadge()
					this.getMessageList()

				}
				app.setMessageTabBarBadge()
			})
		},
bash 复制代码
	setMessageTabBarBadge(){
		app.get('/auth/getUnReadCount', '', '', (res => {
			if(res.data>0){
				uni.setTabBarBadge({
					index: 3,
					text: res.data.toString()
				})
			}else{
				uni.removeTabBarBadge({
					index: 3
				})
			}
		}))
	},

其它需要考虑的就是 退出登录时断开websocket 重新登录时连接

连接时断开重连 断网时重连(目前在退出登录时还有问题)后续会进行优化

关注和im聊天功能基本开发完毕 后续进行细节的完善 和个人资料的自定义

代码地址
https://gitee.com/ddeatrr/springboot_vue_xhs

相关推荐
马优晨6 分钟前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
维天说3 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19943 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
sugar__salt4 小时前
Vue3 表单双向绑定与响应式核心 API 技术详解
前端·javascript·vue.js
噢,我明白了4 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
郝亚军4 小时前
使用Vue 3和Nginx打包和部署Vue.js项目的一般步骤
前端·vue.js·nginx
吠品4 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘4 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记5 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
糖果店的幽灵5 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph