从零用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

相关推荐
啊烨疯狂学java1 小时前
EasyExcel监听器详解
java·easyexcel
北极糊的狐2 小时前
SQL中,# 和 $ 用于不同的占位符语法
java·开发语言
漫漫不慢.3 小时前
九进制转10进制
java·开发语言
别发呆了吧3 小时前
vue路由模式面试题
前端·javascript·vue.js·前端面试题
大小科圣3 小时前
windows配置jdk
java·开发语言
鲤籽鲲3 小时前
C# 内置值类型
android·java·c#
顾北辰204 小时前
利用Deeplearning4j进行 图像识别
java·spring boot·机器学习
虫本初阳4 小时前
【Java】集合中的List【主线学习笔记】
java·学习·list
代码对我眨眼睛4 小时前
vite+vue3动态引入资源文件(问题已解决但离了个大谱)
开发语言·javascript·vue.js
dw52355 小时前
php 根据sdk重命名华为云obs临时访问 url
java·华为云·php