当前位置: 首页 > news >正文

【UniApp开发小程序】私聊功能后端实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】

声明

本文提炼于个人练手项目,其中的实现逻辑不一定标准,实现思路没有参考权威的文档和教程,仅为个人思考得出,因此可能存在较多本人未考虑到的情况和漏洞,因此仅供参考,如果大家觉得有问题,恳请大家指出有问题的地方

如果对客户端的实现感兴趣,可以转身查看【UniApp开发小程序】私聊功能uniapp界面实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】

聊天数据查询管理

数据库设计

【私信表】
在这里插入图片描述

Vo

package com.ruoyi.common.core.domain.vo;import lombok.Data;import java.util.Date;/*** @Author dam* @create 2023/8/22 21:39*/
@Data
public class ChatUserVo {private Long userId;private String userAvatar;private String userName;private String userNickname;/*** 最后一条消息的内容*/private String lastChatContent;/*** 最后一次聊天的日期*/private Date lastChatDate;/*** 未读消息数量*/private Integer unReadChatNum;
}

Controller

其中两个方法较为重要,介绍如下:

  • listChatUserVo:当用户进入消息界面的时候,需要查询出最近聊天的用户,其中还需要展示一些信息,如ChatUserVo的属性
  • listChat:该方法用于查询对方最近和自己的私聊内容,当用户查询了这些私聊内容,默认用户已经看过了,将这些私聊内容设置为已读状态
package com.shm.controller;import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.common.core.domain.entity.Chat;
import com.ruoyi.common.core.domain.vo.ChatUserVo;
import com.shm.service.IChatService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;/*** 聊天数据Controller** @author dam* @date 2023-08-19*/
@RestController
@RequestMapping("/market/chat")
@Api
public class ChatController extends BaseController {@Autowiredprivate IChatService chatService;/*** 查询聊天数据列表*/@PreAuthorize("@ss.hasPermi('market:chat:list')")@GetMapping("/list")public TableDataInfo list(Chat chat) {startPage();List<Chat> list = chatService.list(new QueryWrapper<Chat>(chat));return getDataTable(list);}/*** 查询最近和自己聊天的用户*/@ApiOperation("listChatUserVo")@PreAuthorize("@ss.hasPermi('market:chat:list')")@GetMapping("/listChatUserVo")public TableDataInfo listChatUserVo() {startPage();String username = getLoginUser().getUsername();List<ChatUserVo> list = chatService.listChatUserVo(username);return getDataTable(list);}/*** 查询用户和自己最近的聊天信息*/@ApiOperation("listUsersChatWithMe")@PreAuthorize("@ss.hasPermi('market:chat:list')")@GetMapping("/listChat/{toUsername}")public TableDataInfo listChat(@PathVariable("toUsername") String toUsername) {String curUsername = getLoginUser().getUsername();startPage();List<Chat> list = chatService.listChat(curUsername, toUsername);for (Chat chat : list) {System.out.println("chat:"+chat.toString());}System.out.println();// 查出的数据,如果消息是对方发的,且是未读状态,重新设置为已读List<Long> unReadIdList = list.stream().filter((item1) -> {if (item1.getIsRead() == 0 && item1.getFromWho().equals(toUsername)) {return true;} else {return false;}}).map(item2 -> {return item2.getId();}).collect(Collectors.toList());System.out.println("将"+ unReadIdList.toString()+"设置为已读");if (unReadIdList.size() > 0) {// 批量设置私聊为已读状态chatService.batchRead(unReadIdList);}return getDataTable(list);}/*** 导出聊天数据列表*/@PreAuthorize("@ss.hasPermi('market:chat:export')")@Log(title = "聊天数据", businessType = BusinessType.EXPORT)@PostMapping("/export")public void export(HttpServletResponse response, Chat chat) {List<Chat> list = chatService.list(new QueryWrapper<Chat>(chat));ExcelUtil<Chat> util = new ExcelUtil<Chat>(Chat.class);util.exportExcel(response, list, "聊天数据数据");}/*** 获取聊天数据详细信息*/@PreAuthorize("@ss.hasPermi('market:chat:query')")@GetMapping(value = "/getInfo/{id}")public AjaxResult getInfo(@PathVariable("id") Long id) {return success(chatService.getById(id));}/*** 新增聊天数据*/@PreAuthorize("@ss.hasPermi('market:chat:add')")@Log(title = "聊天数据", businessType = BusinessType.INSERT)@PostMappingpublic AjaxResult add(@RequestBody Chat chat) {return toAjax(chatService.save(chat));}/*** 修改聊天数据*/@PreAuthorize("@ss.hasPermi('market:chat:edit')")@Log(title = "聊天数据", businessType = BusinessType.UPDATE)@PutMappingpublic AjaxResult edit(@RequestBody Chat chat) {return toAjax(chatService.updateById(chat));}/*** 删除聊天数据*/@PreAuthorize("@ss.hasPermi('market:chat:remove')")@Log(title = "聊天数据", businessType = BusinessType.DELETE)@DeleteMapping("/{ids}")public AjaxResult remove(@PathVariable List<Long> ids) {return toAjax(chatService.removeByIds(ids));}
}

Service

package com.shm.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.entity.Chat;
import com.ruoyi.common.core.domain.vo.ChatUserVo;
import com.shm.mapper.ChatMapper;
import com.shm.service.IChatService;
import org.springframework.stereotype.Service;import java.util.List;/*** @author 17526* @description 针对表【chat(聊天数据表)】的数据库操作Service实现* @createDate 2023-08-19 21:12:49*/
@Service
public class IChatServiceImpl extends ServiceImpl<ChatMapper, Chat>implements IChatService {/*** 查询最近和自己聊天的用户** @return*/@Overridepublic List<ChatUserVo> listChatUserVo(String username) {return baseMapper.listChatUserVo(username);}/*** 查询用户和自己最近的聊天信息** @param curUsername* @param toUsername* @return*/@Overridepublic List<Chat> listChat(String curUsername, String toUsername) {return baseMapper.listChat(curUsername, toUsername);}@Overridepublic void batchRead(List<Long> unReadIdList) {baseMapper.batchRead(unReadIdList);}
}

Mapper

package com.shm.mapper;import com.ruoyi.common.core.domain.entity.Chat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.domain.vo.ChatUserVo;
import org.apache.ibatis.annotations.Param;import java.util.List;/**
* @author 17526
* @description 针对表【chat(聊天数据表)】的数据库操作Mapper
* @createDate 2023-08-19 21:12:49
* @Entity com.ruoyi.common.core.domain.entity.Chat
*/
public interface ChatMapper extends BaseMapper<Chat> {List<ChatUserVo> listChatUserVo(@Param("username") String username);List<Chat> listChat(@Param("curUsername") String curUsername, @Param("toUsername") String toUsername);void batchRead(@Param("unReadIdList") List<Long> unReadIdList);
}

【xml文件】

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shm.mapper.ChatMapper"><resultMap id="BaseResultMap" type="com.ruoyi.common.core.domain.entity.Chat"><id property="id" column="id" jdbcType="BIGINT"/><result property="createTime" column="create_time" jdbcType="TIMESTAMP"/><result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/><result property="isDeleted" column="is_deleted" jdbcType="TINYINT"/><result property="fromWho" column="from_who" jdbcType="BIGINT"/><result property="toWho" column="to_who" jdbcType="BIGINT"/><result property="content" column="content" jdbcType="VARCHAR"/><result property="picUrl" column="pic_url" jdbcType="VARCHAR"/></resultMap><sql id="Base_Column_List">id,create_time,update_time,is_deleted,from,to,content,pic_url</sql><update id="batchRead">update chat set is_read = 1 where id in<foreach collection="unReadIdList" item="chatId" separator="," open="(" close=")">#{chatId}</foreach></update><select id="listChatUserVo" resultType="com.ruoyi.common.core.domain.vo.ChatUserVo">SELECT(CASE WHEN c.from_who=#{username} THEN c.to_who ELSE c.from_who END) AS `userName`,c.content AS `lastChatContent`,c.create_time AS lastChatDate,u.user_id AS userId,u.avatar AS userAvatar,u.nick_name AS userNickname,ur.unReadNum as unReadChatNumFROM(SELECTMAX(`id`) AS chatId,CASEWHEN `from_who` = #{username} THEN `to_who`ELSE `from_who`END AS unameFROM `chat`WHERE `from_who` = #{username} OR `to_who` = #{username}GROUP BY uname) AS tINNER JOIN `chat` c ON c.id = t.chatIdLEFT JOIN `sys_user` u ON t.uname = u.user_nameLEFT JOIN (SELECT from_who, SUM(CASE WHEN is_read=1 THEN 0 ELSE 1 END) AS unReadNum FROM chat WHERE is_deleted=0 AND to_who = #{username} GROUP BY from_who) ur ON ur.from_who = t.unameORDER BY c.create_time DESC</select><select id="listChat" resultType="com.ruoyi.common.core.domain.entity.Chat">SELECT*FROMchatWHERE( from_who = #{curUsername} AND to_who = #{toUsername} )OR ( to_who = #{curUsername} AND from_who = #{toUsername} )ORDER BYcreate_time DESC</select>
</mapper>

【查询最近聊天的用户的用户名和那条消息的id】
因为id是自增的,所以最新的那条消息的id肯定最大,因此可以使用MAX(id)来获取最近的消息

SELECT MAX(`id`) AS chatId,CASE WHEN `from_who` = 'admin' THEN `to_who`ELSE `from_who`END AS unameFROM `chat`WHERE `from_who` = 'admin' OR `to_who` = 'admin'GROUP BY uname

在这里插入图片描述
【内连接私信表获取消息的其他信息】

INNER JOIN `chat` c ON c.id = t.chatId 

【左连接用户表获取用户的相关信息】

LEFT JOIN `sys_user` u ON t.uname = u.user_name

【左联接私信表获取未读对方消息的数量】
CASE WHEN is_read=1 THEN 0 ELSE 1 END 如果已读,说明未读数量为0;否则为1

LEFT JOIN (SELECT from_who, SUM(CASE WHEN is_read=1 THEN 0 ELSE 1 END) AS unReadNum FROM chat WHERE is_deleted=0 AND to_who = 'admin' GROUP BY from_who) ur ON ur.from_who = t.uname

【最后按照用户和自己最后聊天的时间来降序排序】

ORDER BY c.create_time DESC

WebSocket引入

为什么使用WebSocket

WebSocket不仅支持客户端向服务端发送消息,同时也支持服务端向客户端发送消息,这样才能完成私聊的功能。即
用户1-->服务端-->用户2

依赖

<!-- websocket -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置类

package com.shm.config;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
public class WebSocketConfig {/*** 注入一个ServerEndpointExporter,* 该Bean会自动注册使用@ServerEndpoint注解 声明的websocket endpoint*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}

WebSocket服务

需要注意的是,Websocket是多例模式,无法直接使用@Autowired注解来注入rabbitTemplate,需要使用下面的方式,其中rabbitTemplate为静态变量

private static RabbitTemplate rabbitTemplate;@Autowiredpublic void setRabbitTemplate(RabbitTemplate rabbitTemplate) {WebSocketServer.rabbitTemplate = rabbitTemplate;}
package com.shm.component;import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.entity.Chat;
import com.shm.component.delay.DelayQueueManager;
import com.shm.component.delay.DelayTask;
import com.shm.constant.RabbitMqConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @author websocket服务*/
@ServerEndpoint(value = "/websocket/{username}")
@Component//将WebSocketServer注册为spring的一个bean
public class WebSocketServer {private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);/*** 记录当前在线连接的客户端的session*/public static final Map<String, Session> usernameAndSessionMap = new ConcurrentHashMap<>();/*** 记录正在进行的聊天的发出者和接收者*/public static final Map<String, Integer> fromToMap = new ConcurrentHashMap<>();/*** 用户Session保留时间,如果超过该时间,用户还没有给服务端发送消息,认为用户下线,删除其Session* 注意:该时间需要比客户端的心跳时间更长*/private static final long expire = 6000;// websocket为多例模式,无法直接注入,需要换成下面的方式
//    @Autowired
//    RabbitTemplate rabbitTemplate;private static RabbitTemplate rabbitTemplate;@Autowiredpublic void setRabbitTemplate(RabbitTemplate rabbitTemplate) {WebSocketServer.rabbitTemplate = rabbitTemplate;}@Autowiredprivate static DelayQueueManager delayQueueManager;@Autowiredpublic void setDelayQueueManager(DelayQueueManager delayQueueManager) {WebSocketServer.delayQueueManager = delayQueueManager;}/*** 浏览器和服务端连接建立成功之后会调用这个方法*/@OnOpenpublic void onOpen(Session session, @PathParam("username") String username) {usernameAndSessionMap.put(username, session);// 建立延时任务,如果到expire时间,客户端还是没有和服务器有任何交互的话,就删除该用户的session,表示该用户下线delayQueueManager.put(new DelayTask(username, expire));log.info("有新用户加入,username={}, 当前在线人数为:{}", username, usernameAndSessionMap.size());}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(Session session, @PathParam("username") String username) {usernameAndSessionMap.remove(username);log.info("有一连接关闭,移除username={}的用户session, 当前在线人数为:{}", username, usernameAndSessionMap.size());}/*** 发生错误的时候会调用这个方法*/@OnErrorpublic void onError(Session session, Throwable error) {log.error("发生错误");error.printStackTrace();}/*** 服务端发送消息给客户端*/public void sendMessage(String message, Session toSession) {try {log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);toSession.getBasicRemote().sendText(message);} catch (Exception e) {log.error("服务端发送消息给客户端失败", e);}}/*** onMessage方法是一个消息的中转站* 1、首先接受浏览器端socket.send发送过来的json数据* 2、然后解析其数据,找到消息要发送给谁* 3、最后将数据发送给相应的人** @param message 客户端发送过来的消息 数据格式:{"from":"user1","to":"admin","text":"你好呀"}*/@OnMessagepublic void onMessage(String message, Session session, @PathParam("username") String username) {
//        log.info("服务端接收到 {} 的消息,消息内容是:{}", username, message);// 收到用户的信息,删除之前的延时任务,创建新的延时任务delayQueueManager.put(new DelayTask(username, expire));if (!usernameAndSessionMap.containsKey(username)) {// 可能用户挂机了一段时间,被下线了,后面又重新回来发信息了,需要重新将用户和session添加字典中usernameAndSessionMap.put(username, session);}// 将json字符串转化为json对象JSONObject obj = JSON.parseObject(message);String status = (String) obj.get("status");// 获取消息的内容String text = (String) obj.get("text");// 查看消息要发送给哪个用户String to = (String) obj.get("to");String fromToKey = username + "-" + to;String toFromKey = to + "-" + username;if (status != null) {if (status.equals("start")) {fromToMap.put(fromToKey, 1);} else if (status.equals("end")) {System.out.println("移除销毁的fromToKey:" + fromToKey);fromToMap.remove(fromToKey);} else if (status.equals("ping")) {// 更新用户对应的时间戳
//                usernameAndTimeStampMap.put(username, System.currentTimeMillis());}} else {// 封装数据发送给消息队列Chat chat = new Chat();chat.setFromWho(username);chat.setToWho(to);chat.setContent(text);chat.setIsRead(0);//        chat.setPicUrl("");// 根据to来获取相应的session,然后通过session将消息内容转发给相应的用户Session toSession = usernameAndSessionMap.get(to);if (toSession != null) {JSONObject jsonObject = new JSONObject();// 设置消息来源的用户名jsonObject.put("from", username);// 设置消息内容jsonObject.put("text", text);// 服务端发送消息给目标客户端this.sendMessage(jsonObject.toString(), toSession);log.info("发送消息给用户 {} ,消息内容是:{} ", toSession, jsonObject.toString());if (fromToMap.containsKey(toFromKey)) {chat.setIsRead(1);}} else {log.info("发送失败,未找到用户 {} 的session", to);}rabbitTemplate.convertAndSend(RabbitMqConstant.CHAT_STORAGE_EXCHANGE, RabbitMqConstant.CHAT_STORAGE_ROUTER_KEY, chat);}}}

RabbitMQ引入

为什么使用消息队列

在用户之间进行聊天的时候,需要将用户的聊天数据存储到数据库中,但是如果大量用户同时在线的话,可能同一时间发送的消息数量太多,如果同时将这些消息存储到数据库中,会给数据库带来较大的压力,使用RabbitMQ可以先把要存储的数据放到消息队列,然后数据库服务器压力没这么大的时候,就会从消息队列中获取数据来存储,这样可以分散数据库的压力。但是如果用户是直接从数据库获取消息的话,消息可能有一定的延迟,如果用户之间正在聊天的话,消息则不会延迟,因为聊天内容会立刻通过WebSocket发送给对方。

依赖

<!-- rabbitMQ-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

启动类添加注解

在启动类的上方添加@EnableRabbit注解
在这里插入图片描述

常量类

因为有多处会使用到队列命名等信息,创建一个常量类来保存相关信息

package com.shm.constant;public class RabbitMqConstant {public static final String CHAT_STORAGE_QUEUE = "shm.chat-storage.queue";public static final String CHAT_STORAGE_EXCHANGE = "shm.chat-storage-event-exchange";public static final String CHAT_STORAGE_ROUTER_KEY = "shm.chat-storage.register";
}

使用配置类创建队列、交换机、绑定关系

package com.shm.config;import com.shm.constant.RabbitMqConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyRabbitConfig {/*** 使用JSON序列化机制,进行消息转换* @return*/@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 私信存储队列** @return*/@Beanpublic Queue chatStorageQueue() {Queue queue = new Queue(RabbitMqConstant.CHAT_STORAGE_QUEUE, true, false, false);return queue;}/*** 私信存储交换机* 创建交换机,由于只需要一个队列,创建direct交换机** @return*/@Beanpublic Exchange chatStorageExchange() {//durable:持久化return new DirectExchange(RabbitMqConstant.CHAT_STORAGE_EXCHANGE, true, false);}/*** 创建私信存储 交换机和队列的绑定关系** @return*/@Beanpublic Binding chatStorageBinding() {return new Binding(RabbitMqConstant.CHAT_STORAGE_QUEUE,Binding.DestinationType.QUEUE,RabbitMqConstant.CHAT_STORAGE_EXCHANGE,RabbitMqConstant.CHAT_STORAGE_ROUTER_KEY,null);}}

消息监听器

创建一个消息监听类来监听队列的消息,然后调用相关的逻辑来处理信息,本文主要的处理是将私信内容存储到数据库中

package com.shm.listener;import com.rabbitmq.client.Channel;
import com.ruoyi.common.core.domain.entity.Chat;
import com.shm.constant.RabbitMqConstant;
import com.shm.service.IChatService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;@Service
/*** 注意,类上面需要RabbitListener注解*/
@RabbitListener(queues = RabbitMqConstant.CHAT_STORAGE_QUEUE)
public class ChatStorageListener {@Autowiredprivate IChatService chatService;@RabbitHandlerpublic void handleStockLockedRelease(Chat chat, Message message, Channel channel) throws IOException {try {boolean save = chatService.save(chat);//解锁成功,手动确认,消息才从MQ中删除channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);} catch (Exception e) {//只要有异常,拒绝消息,让消息重新返回队列,让别的消费者继续解锁channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);}}}

发送消息到消息队列

WebSocketServer为Websocket后端服务代码,其中的onMessage方法会接受客户端发送过来的消息,当接收到消息的时候,将消息发送给消息队列

// 封装数据发送给消息队列
Chat chat = new Chat();
chat.setFromWho(username);
chat.setToWho(to);
chat.setContent(text);
chat.setPicUrl("");
rabbitTemplate.convertAndSend(RabbitMqConstant.CHAT_STORAGE_EXCHANGE,RabbitMqConstant.CHAT_STORAGE_ROUTER_KEY,chat);

延时任务

为什么使用延时任务

为了更好地感知用户的在线状态,在用户连接了WebSocket或者发送消息之后,建立一个延时任务,如果到达了所设定的延时时间,就删除用户的Session,认为用户已经下线;如果在延时期间之内,用户发送了新消息,或者发送了心跳信号,证明该用户还处于在线状态,删除前面的延时任务,并创建新的延时任务

延时任务类

package com.shm.component.delay;import lombok.Data;
import lombok.Getter;import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;/*** @Author dam* @create 2023/8/25 15:12*/
@Getter
public class DelayTask implements Delayed {/*** 用户名*/private final String userName;/*** 任务的真正执行时间*/private final long executeTime;/*** 任务延时多久执行*/private final long expire;/*** @param expire 任务需要延时的时间*/public DelayTask(String userName, long expire) {this.userName = userName;this.executeTime = expire + System.currentTimeMillis();this.expire = expire;}/*** 根据给定的时间单位,返回与此对象关联的剩余延迟时间* * @param unit the time unit 时间单位* @return 返回剩余延迟,零值或负值表示延迟已经过去*/@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(this.executeTime - System.currentTimeMillis(), unit);}@Overridepublic int compareTo(Delayed o) {return 0;}
}

延时任务管理

package com.shm.component.delay;import com.shm.component.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executors;/*** @Author dam* @create 2023/8/25 15:12*/
@Component
@Slf4j
public class DelayQueueManager implements CommandLineRunner {private final DelayQueue<DelayTask> delayQueue = new DelayQueue<>();private final Map<String, DelayTask> usernameAndDelayTaskMap = new ConcurrentHashMap<>();/*** 加入到延时队列中** @param task*/public void put(DelayTask task) {// 因为一个用户只能对应一个延时任务,所以如果已经存在了延时任务,将其进行删除if (usernameAndDelayTaskMap.containsKey(task.getUserName())) {this.remove(task.getUserName());}delayQueue.put(task);usernameAndDelayTaskMap.put(task.getUserName(), task);}/*** 取消延时任务** @param username 要删除的任务的用户名* @return*/public boolean remove(String username) {DelayTask remove = usernameAndDelayTaskMap.remove(username);return delayQueue.remove(remove);}@Overridepublic void run(String... args) throws Exception {this.executeThread();}/*** 延时任务执行线程*/private void executeThread() {while (true) {try {DelayTask task = delayQueue.take();//执行任务processTask(task);} catch (InterruptedException e) {break;}}}/*** 执行延时任务** @param task*/private void processTask(DelayTask task) {// 删除该用户的session,表示用户下线WebSocketServer.usernameAndSessionMap.remove(task.getUserName());log.error("执行定时任务:{}下线", task.getUserName());}}

相关文章:

【UniApp开发小程序】私聊功能后端实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】

声明 本文提炼于个人练手项目&#xff0c;其中的实现逻辑不一定标准&#xff0c;实现思路没有参考权威的文档和教程&#xff0c;仅为个人思考得出&#xff0c;因此可能存在较多本人未考虑到的情况和漏洞&#xff0c;因此仅供参考&#xff0c;如果大家觉得有问题&#xff0c;恳…...

运维高级学习--Kubernetes(K8s 1.28.x)部署

一、基础环境配置&#xff08;所有主机操作&#xff09; 主机名规划 序号 主机ip 主机名规划1 192.168.1.30 kubernetes-master.openlab.cn kubernetes-master2 192.168.1.31 kubernetes-node1.openlab.cn kubernetes-node13 192.168.1.32 kubernetes-node2…...

Apache zookeeper kafka 开启SASL安全认证 —— 筑梦之路

简介 Kafka是一个高吞吐量、分布式的发布-订阅消息系统。Kafka核心模块使用Scala语言开发&#xff0c;支持多语言&#xff08;如Java、Python、Go等&#xff09;客户端&#xff0c;它可以水平扩展和具有高吞吐量特性而被广泛使用&#xff0c;并与多类开源分布式处理系统进行集成…...

lintcode 1017 · 相似的RGB颜色【进制计算】

题目链接&#xff0c;题目描述 https://www.lintcode.com/problem/1017 在本题中&#xff0c;每个大写字母代表从“0”到“f”的一些十六进制数字。红绿蓝三元色#AABBCC可以简写为#ABC。 例如&#xff0c;#15c是颜色#1155cc的简写。现在&#xff0c;定义两种颜色#ABCDEF和#UV…...

全国首台!浙江机器人产业集团发布垂起固定翼无人机-机器人自动换电机巢

展示突破性创新技术&#xff0c;共话行业发展趋势。8月25日&#xff0c;全国首台垂起固定翼无人机-机器人自动换电机巢新品发布会暨“科创中国宁波”无人机产业趋势分享会在余姚市机器人小镇成功举行。 本次活动在宁波市科学技术协会、余姚市科学技术协会指导下&#xff0c;由浙…...

采用 UML 对软件系统进行建模的基本框架

UML 包括一些可以相互组合为图标的图形元素&#xff0c; 通过提供不同形式的图形来 表述从软件分析开始的软件开发全过程的描述&#xff0c;一个图就是系统架构在某个侧面的 表示&#xff0c;所有的图组成了系统的完整视图。UML 主要提供了以下五类图&#xff1a; &#xff…...

编译tiny4412 Linux 内核

工作环境 Ubuntu 22 交叉编译器 4.5.1 解压Linux内核源码&#xff0c;进入目录 将官方配置完好的defconfig文件作为配置文件 cp tiny4412_linux_defconfig .config由于内核版本较低&#xff0c;需要下载低版本的gcc&#xff0c;选择下载gcc-9与g9 sudo apt install gcc-9 g-…...

Ubuntu22.04安装中文输入法►由踩坑到上岸版◄

Ubuntu22.04安装中文输入法►由踩坑到上岸版◄ 了解入坑上岸 更新一发&#xff1a;Gedit中文乱码问题的解决 为了方便回忆和记录甚至后面继续重装系统&#xff0c;我还是写一下以便将来用到或参考&#xff5e; 了解 安装Ubuntu22.04&#xff08;截至2023年08月26日11&#xff…...

SpringBoot简单上手

spring boot 是spring快速开发脚手架&#xff0c;通过约定大于配置&#xff0c;优化了混乱的依赖管理&#xff0c;和复杂的配置&#xff0c;让我们用java-jar方式,运行启动java web项目 入门案例 创建工程 先创建一个空的工程 创建一个名为demo_project的项目&#xff0c;并且…...

git及GitHub的使用

文章目录 git在本地仓库的使用github使用创建仓库https协议连接(不推荐&#xff0c;现在用起来比较麻烦)ssh连接&#xff08;推荐&#xff09;git分支操作冲突处理忽略文件 git在本地仓库的使用 1.在目标目录下右键打开git bash here 2.创建用户名和邮箱(注&#xff1a; 下载完…...

【考研数学】线性代数第四章 —— 线性方程组(1,基本概念 | 基本定理 | 解的结构)

文章目录 引言一、线性方程组的基本概念与表达形式二、线性方程组解的基本定理三、线性方程组解的结构写在最后 引言 继向量的学习后&#xff0c;一鼓作气&#xff0c;把线性方程组也解决了去。O.O 一、线性方程组的基本概念与表达形式 方程组 称为 n n n 元齐次线性方程组…...

使用Python写入数据到Excel:实战指南

在数据科学领域&#xff0c;Excel是一种广泛使用的电子表格工具&#xff0c;可以方便地进行数据管理和分析。然而&#xff0c;当数据规模较大或需要自动化处理时&#xff0c;手动操作Excel可能会变得繁琐。此时&#xff0c;使用Python编写程序将数据写入Excel文件是一个高效且便…...

接口测试总结分享(http与rpc)

接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑依赖关系等。 一、了解一下HTTP与RPC 1. HTTP&#xff08;H…...

数据结构(Java实现)LinkedList与链表(下)

** ** 结论 让一个指针从链表起始位置开始遍历链表&#xff0c;同时让一个指针从判环时相遇点的位置开始绕环运行&#xff0c;两个指针都是每次均走一步&#xff0c;最终肯定会在入口点的位置相遇。 LinkedList的模拟实现 单个节点的实现 尾插 运行结果如下&#xff1a; 也…...

linux查看正在运行的nginx在哪个文件夹当中

1、查出Nginx进程PID ps -ef|grep nginx2、查看Nginx进程启动时的工作目录 ls -la /proc/<PID>/cwd将<PID>替换为第一步中列出的Nginx进程的PID。该命令会显示Nginx进程在启动时所在的工作目录&#xff08;当前工作目录&#xff09;...

Vue实现Excel表格中按钮增加小数位数,减少小数位数功能,多用于处理金融数据

效果图 <template><div><el-button click"increaseDecimals">A按钮</el-button><el-button click"roundNumber">B按钮</el-button><el-table :data"tableData" border><el-table-column v-for&q…...

自然语言处理(一):词嵌入

词嵌入 词嵌入&#xff08;Word Embedding&#xff09;是自然语言处理&#xff08;NLP&#xff09;中的一种技术&#xff0c;用于将文本中的单词映射到一个低维向量空间中。它是将文本中的单词表示为实数值向量的一种方式。 在传统的文本处理中&#xff0c;通常使用独热编码&…...

【HSPCIE仿真】HSPICE仿真基础

HSPICE概述 1. HSPICE简介3. 标准输入文件4. 标准输出文件3. HSPCIE仿真过程 1. HSPICE简介 SPICE &#xff08;Simulation Program with IC Emphasis&#xff09;是1972 年美国加利福尼亚大学柏克莱分校电机工程和计算机科学系开发 的用于集成电路性能分析的电路模拟程序。 …...

二、前端监控之方案调研

前端监控体系 一个完整的前端监控体系包括了日志采集、日志上报、日志存储、日志切分&计算、数据分析、告警等流程。 对于一名前端开发工程师来说&#xff0c;也就意味着工作不再局限于前端业务的开发工作&#xff0c;需要有Nginx服务运维能力、实时/离线分析能力、Node应…...

npm 创建 node.js 项目

package.json重要说明 package.json是创建任何node.js项目必须要有的一个文件。 因为在package.json文件中&#xff0c;有详细的项目描述&#xff0c; 包括&#xff1a; (1)项目名称&#xff1a;name (2)版本&#xff1a;version (3)依赖文件&#xff1a;dependencies 等…...

JMeter性能测试(上)

一、基础简介 界面 打开方式 双击 jmeter.bat双击 ApacheJMeter.jsr命令行输入 java -jar ApacheJMeter.jar 目录 BIN 目录&#xff1a;存放可执行文件和配置文件 docs目录&#xff1a;api文档&#xff0c;用于开发扩展组件 printable-docs目录&#xff1a;用户帮助手册 li…...

自定义date工具类 DateUtils.java

自定义date工具类 DateUtils.java 简介 Date日期类型的工具类。 api 日期格式化 format(Date date)&#xff1b;日期格式化 format(Date date, String pattern)&#xff1b;计算距离现在多久&#xff0c;非精确 getTimeBefore(Date date);计算距离现在多久&#xff0c;精确…...

Linux(Ubuntu)安装docker

2017年的3月1号之后&#xff0c;Docker 的版本命名开始发生变化&#xff0c;同时将 CE 版本和 EE 版本进行分开。 Docker社区版&#xff08;CE&#xff09;&#xff1a;为了开发人员或小团队创建基于容器的应用,与团队成员分享和自动化的开发管道。docker-ce 提供了简单的安装…...

Apache Poi 实现Excel多级联动下拉框

由于最近做的功能&#xff0c;需要将接口返回的数据列表&#xff0c;输出到excel中&#xff0c;以供后续导入&#xff0c;且网上现有的封装&#xff0c;使用起来都较为麻烦&#xff0c;故参考已有做法封装了工具类。 使用apache poi实现excel联动下拉框思路 创建隐藏单元格&a…...

常见的 HTML<meta> 标签的 name 属性及其作用

HTML中的 <meta> 标签可以通过 name 属性提供元数据&#xff0c;这些元数据可以用于指定有关文档的信息&#xff0c;以及控制浏览器和搜索引擎的行为。name 属性通常与其他属性一起使用&#xff0c;如 content、charset、http-equiv 等&#xff0c;以提供更具体的元数据信…...

【网络安全】理解报文加密、数字签名能解决的实际问题

文章目录 前言1. 防止报文泄露 —— 加密体系的出现1.1 理解非对称加密体系的实施难点1.2 加密体系的实际应用 2. 防止报文被篡改 —— 数字签名的出现2.1 数字签名的原理2.2 数字签名的实施难点2.2 数字签名的实际应用 —— 引入摘要算法 3. 实体鉴别 —— CA证书 后记 前言 …...

linux中安装nodejs,卸载nodejs,更新nodejs

卸载nodejs 卸载node sudo apt-get remove nodejs清理掉自动安装的并且不需要软件包 sudo apt autoremove查看node相关的文件 sudo whereis node如果有文件需要手动删除文件 删除该文件命令 sudo rm -rf /usr/local/bin/node在此查看node -v 是未找到&#xff0c;说明你已经…...

浅谈Python网络爬虫应对反爬虫的技术对抗

在当今信息时代&#xff0c;数据是非常宝贵的资源。而作为一名专业的 Python 网络爬虫程序猿&#xff0c;在进行网页数据采集时经常会遭遇到各种针对爬虫行为的阻碍和限制&#xff0c;这就需要我们掌握一些应对反爬机制的技术手段。本文将从不同层面介绍如何使用 Python 进行网…...

代理池在过程中一直运行

Hey&#xff0c;爬虫达人们&#xff01;在爬虫的过程中&#xff0c;要保持代理池的稳定性可不容易。今天就来和大家分享一些实用经验&#xff0c;教你如何让代理池在爬虫过程中一直运行&#xff01;方法简单易行&#xff0c;让你的爬虫工作更顺畅. 在进行爬虫工作时&#xff0…...

基于Java+SpringBoot+Vue前后端分离党员教育和管理系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…...

axure rp怎么做网站/注册网站流程和费用

概述&#xff1a;本章介绍磁盘分区和文件系统&#xff0c;深度了解linux存储数据所依赖到各个部分模块。设备文件I/O Ports: I/O设备地址一切皆文件&#xff1a;Linux为所有的设备文件都提供了统一的操作函数接口&#xff0c;方法是使用数据结构struct file_operations。这个数…...

做网站得做多少网页/中国公关公司前十名

MineCraft是一款专门为用户设计的模拟经营提醒&#xff0c;以建设和破坏为主&#xff0c;去创造属于你自己的世界&#xff0c;此软件主要是帮助玩家搭建我的世界服务器端&#xff0c;让你从此联机乐无穷&#xff0c;你还在等什么赶紧下载吧&#xff01;游戏介绍《MineCraft》是…...

数据分析案例网站/无锡今日头条新闻

服务器终端输入命令 jupyter notebook --no-browser --port8889 本地终端输入命令 ssh -N -f -L localhost:8888:localhost:8889 usernameip username和ip替换为服务器的username和ip地址 本地打开浏览器&#xff0c;http://localhost:8888/ 即可访问 可能出现的问题 如果ju…...

网上书店网站建设毕业设计/百度推广信息流有用吗

SAP Cloud for Customer的Sales工作中心里有Sales Quote和Sales Order两个视图&#xff0c;一个用于销售报价单&#xff0c;另一个用于销售订单。 流程上是先有报价单 &#xff0c;报价单是一份OFFER&#xff0c;并不具备法律效力&#xff0c;只有在生成销售订单后&#xff0c;…...

wordpress 后台登陆 修改/google首页

在是用虚拟机的时候&#xff0c;往往时间对不上&#xff0c;这就使强迫症不得安心学习了&#xff0c;解决方法如下&#xff1a; 作为中国的一员&#xff0c;在安装虚拟机的时候&#xff0c;可以设置上海时间作为参考&#xff1a; 1.把上海时间文件cp到etc文件下的localtime文…...

中国住房和城乡建设网官网/seo排名优化的方法

1、首先数组首元素的地址和数组地址的值是相等的。2、数组首元素的地址和数组地址是两个不同的概念。3、举例如下&#xff1a;stdio.h#includevoid main() { int a[10] {}; printf("%d\n", a); printf("%d\n", a1); //与前面一个相差4 printf(&quo…...