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

前言

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

前面的文章我们主要完成了笔记的点赞和收藏及留言功能 今天我们讲解点赞关注 im 聊天功能
关注
个人名片页
我们需要有一个关注的操作 这里我们复用个人中心页面
按钮会有三种形式 关注 取消关注 互相关注三种样式

			<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>

关注和点赞功能实现原理大致相同 只不过有一个互相关注
后台先创建一个关注表

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 = #{当前登录人}
这样还不行 因为只查询到了粉丝列表
所以需要加一个子查询 我是否关注了对方 这样才能实现互关效果

<!--    查询关注我的所有列表信息 并在查询的时候设置子查询 查询当前用户作为关注者的时候有没有关注对方-->
    <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的依赖

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

配置类

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

处理点对点聊天的接口

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端口配置

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 可用的 连接类 实现了心跳连接

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 分别查询到相应的设备集合
然后进行发送即可 为什么是集合 因为 一个账号可能涉及到多端登录
数据推送到前端后

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

页面会向下继续生成 进行页面滚动即可
当页面聊天记录过多时 我们考虑分页加载聊天数据
这里和我们的笔记页表相同 下拉时 页数增加 没有数据是提示用户

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分钟是刚刚
  • 今天的数据显示 时分
  • 近一周的显示星期数 时分
  • 本年的显示月日 时分
  • 本年之前的 显示 年月日 时分
  • 小于两分钟的数据 创建时间只显示第一条
		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;
		},
	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进行分组
    <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数量
		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()
			})
		},
	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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/947091.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PP模块部分BAPI函数

工艺路线 BAPI_ROUTING_CREATE 创建工艺路线 BAPI_ROUTING_EXISTENCE_CHECK 检查工艺路线是否存在 参考操作集 BAPI_REFSETOFOPERATIONS_CREATE 创建参考操作集 BAPI_REFSETOFOPR_EXISTENCE_CHK 检查参考操作集是否存在 计划订单 BAPI_PLANNEDORDER_CREATE 创建计划订单 BAPI…

Tomcat解析

架构图 核心功能 Tomcat是Apache开源的轻量级Java ServletServlet容器&#xff0c;其中一个Server&#xff08;Tomcat实例&#xff09;可以管理多个Service&#xff08;服务&#xff09;&#xff0c;一个Service包含多个Connector和一个Engine&#xff0c;负责管理请求到应用的…

【openGauss】正则表达式次数符号“{}“在ORACLE和openGauss中的差异

一、前言 正则作为一种常用的字符串处理方式&#xff0c;在各种开发语言&#xff0c;甚至数据库中&#xff0c;都有自带的正则函数。但是正则函数有很多标准&#xff0c;不同标准对正则表达式的解析方式不一样&#xff0c;本次在迁移一个ORACLE数据库到openGauss时发现了一个关…

C# 修改项目类型 应用程序程序改类库

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

DataV数据可视化

阿里云 DataV 是一个强大的数据可视化工具&#xff0c;可以帮助用户通过创建丰富的图表、仪表盘、地图和互动视图&#xff0c;将复杂的数据转化为易于理解和分析的可视化信息。DataV主要用于大数据和实时数据的展示&#xff0c;可以帮助企业和个人更直观地理解数据背后的含义&a…

STM32-笔记18-呼吸灯

1、实验目的 使用定时器 4 通道 3 生成 PWM 波控制 LED1 &#xff0c;实现呼吸灯效果。 频率&#xff1a;2kHz&#xff0c;PSC71&#xff0c;ARR499 利用定时器溢出公式 周期等于频率的倒数。故Tout 1/2KHZ&#xff1b;Ft 72MHZ PSC71&#xff08;喜欢设置成Ft的倍数&…

【20250101】Nature正刊:纯仿真强化学习得到外骨骼机器人的自适应控制策略

基本信息 论文标题&#xff1a;Experiment-free exoskeleton assistance via learning in simulation 发表期刊&#xff1a;Nature 发表时间&#xff1a;2024年6月12日 访问链接 论文&#xff1a;https://www.nature.com/articles/s41586-024-07382-4?fromPaywallRectrue 代…

【从零开始】11. LLaMA-Factory 微调 Qwen 模型(番外篇)

书接上回&#xff0c;在完成了 RAGChecker 测试后&#xff0c;离 RAG 应用真正发布还差最后一步 - 基础信息指令微调。考虑到模型还是需要具备一定程度的“自我认知”&#xff0c;因此需要将公司信息“嵌入”到模型里面的。为此&#xff0c;我选择了 LLaMA-Factory&#xff08;…

PowerShell 信息,请参阅 https......_Execution_Policies

文章目录 1. 问题分析2. 检查当前执行策略3. 解决方案&#xff1a;更改执行策略4. 若问题依然存在5. 注意事项 信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about _Execution_Policies。 所在位置 行:1 字符: 3. D:\Users\Documents\WindowsPo…

二、github基础

Github基础 备用github.com网站一、用户界面-Overview&#xff08;概览&#xff09;1用户信息2 导航栏3 热门仓库4 贡献设置5贡献活动6搜索和筛选7自定义收藏8贡献统计9最近活动10其他链接 二、用户界面-Repositories&#xff08;仓库&#xff09;1 libusb_stm322 savedata3 Fi…

C语言----指针

目录 1.概念 2.格式 3.指针操作符 4.初始化 1. 将普通变量的地址赋值给指针变量 a. 将数组的首地址赋值给指针变量 b. 将指针变量里面保存的地址赋值给另一个指针变量 5.指针运算 5.1算术运算 5.2 关系运算 指针的大小 总结&#xff1a; 段错误 指针修饰 1. con…

青少年编程与数学 02-006 前端开发框架VUE 01课题、VUE简介

青少年编程与数学 02-006 前端开发框架VUE 01课题、VUE简介 一、前端开发一、前端框架二、Vue.js三、应用场景四、区别五、学习资源六、应用示例说明&#xff1a; 课题摘要:本文介绍了前端开发框架Vue.js的基本概念和特性。Vue.js是一个渐进式JavaScript框架&#xff0c;用于构…

STM32单片机芯片与内部57 SPI 数据手册 寄存器

目录 一、SPI寄存器 1、SPI控制寄存器 1(SPI_CR1)(I2S模式下不使用) 2、SPI控制寄存器 2(SPI_CR2) 3、SPI 状态寄存器(SPI_SR) 4、SPI 数据寄存器(SPI_DR) 5、SPI CRC多项式寄存器(SPI_CRCPR)(I2S模式下不使用&#xff09; 6、SPI Rx CRC寄存器(SPI_RXCRCR)(I2S模式下不…

QT-------------自定义插件和库

以下是一个使用 Qt 实现图表交互操作的示例&#xff0c;涵盖了自定义图表视图类、不同类型的柱状图和饼图等内容。 实现思路 自定义图表视图类&#xff1a;创建一个从 QChartView 派生的自定义类&#xff0c;用于处理图表的交互操作。主窗口设计初始化&#xff1a;在主窗口中…

【Python】闭包

闭包&#xff08;Closure&#xff09;是指一个函数记住了并可以访问它的词法作用域&#xff08;lexical scope&#xff09;&#xff0c;即使这个函数在词法作用域之外执行。 闭包其实就是延伸了作用域的函数&#xff0c;包括被延伸函数主体中引用的非全局变量和局部变量。这些…

矩阵运算提速——玩转opencv::Mat

介绍:用Eigen或opencv::Mat进行矩阵的运算&#xff0c;比用cpp的vector或vector进行矩阵运算要快吗? 使用 Eigen 或 OpenCV 的 cv::Mat 进行矩阵运算通常比使用 std::vector<int> 或 std::vector<double> 更快。这主要有以下几个原因&#xff1a; 优化的底层实现…

FastDeploy部署paddlecls分类模型(windows)

目录 写在前面 总体步骤 C SDK编译库 方式1&#xff1a;编译安装 方式2&#xff1a;下载预编译库 准备模型、文件、代码和数据 模型文件类型 samples代码 待预测图像 使用 FastDeploy C SDK 将cpp源码编译为exe 编写cpp代码 cpp代码编译exe 运行可执行程序exe 将…

【第二部分--Python之基础】03 容器类型的数据

Python内置的数据类型如序列&#xff08;列表、元组等&#xff09;、集合和字典等可以容纳多项数据&#xff0c;我们称它们为容器类型的数据。 序列 序列&#xff08;sequence&#xff09;是一种可迭代的、元素有序的容器类型的数据。 序列包括列表&#xff08;list&#xff…

linux shell脚本 【分支结构case...in 、循环结构、函数】内附练习

1.思维导图 2.练习 1.定义一个find函数&#xff0c;查找ubuntu和root的gid 2.定义一个数组&#xff0c;写一个函数完成对数组的冒泡排序 bubble() {n${#arr[*]}for((i0;i<n-1;i));dofor((j0;j<n-1-i;j));doif ((arr[j]>arr[j1]));thentemp${arr[j]}arr[j]${arr[j1]}a…

基于SpringBoot和OAuth2,实现通过Github授权登录应用

基于SpringBoot和OAuth2&#xff0c;实现通过Github授权登录应用 文章目录 基于SpringBoot和OAuth2&#xff0c;实现通过Github授权登录应用0. 引言1. 创建Github应用2. 创建SpringBoot测试项目2.1 初始化项目2.2 设置配置文件信息2.3 创建Controller层2.4 创建Html页面 3. 启动…