spring boot 与 iview 前后端分离架构之基于socketIo的消息推送的实现(三十三)
- 公众号
- websocket的实现
websocket的实现
bug修复
在前面我们已经实现了类oauth2的鉴权机制,但是当时我们的前端少写了刷新token的地址,因此在三十三章之前我们都是无法在页面端实现token的刷新,因此本章我们补足该缺陷,同时配置本章websocket的地址,修改config目录底下的dev.js,改造完成以后的代码如下:
export const runDevConfig = {
baseUrl: 'http://127.0.0.1:8288',
imgUrl: 'http://127.0.0.1:80/merJoinResource',
refreshTokenUrl: 'http://127.0.0.1:8288/user/refreshToken',
socketUrl: 'http://127.0.0.1:9099'
};
后端改造
socketIo的后端实现
在config包底下创建一个socketio的包,在该包中添加以下的实现
PushMessage
package com.github.bg.admin.core.config.socketio;
/**
* @author linzf
* @since 2019-06-13
* 类描述:socket消息发送实体类
*/
public class PushMessage {
/**
* 登录socket的socketToken
*/
private String socketToken;
/**
* 推送的内容
*/
private String content;
/**
* 空的构造函数
*/
public PushMessage() {
super();
}
/**
* 构造函数
* @param socketToken
* @param content
*/
public PushMessage(String socketToken, String content) {
this.socketToken = socketToken;
this.content = content;
}
public String getSocketToken() {
return socketToken;
}
public void setSocketToken(String socketToken) {
this.socketToken = socketToken;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
SocketIoConfig
package com.github.bg.admin.core.config.socketio;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author linzf
* @since 2019-06-13
* 类描述:socketIo的配置类
*/
@Configuration
public class SocketIoConfig {
@Value("${socketio.host}")
private String host;
@Value("${socketio.port}")
private Integer port;
@Value("${socketio.bossCount}")
private int bossCount;
@Value("${socketio.workCount}")
private int workCount;
@Value("${socketio.allowCustomRequests}")
private boolean allowCustomRequests;
@Value("${socketio.upgradeTimeout}")
private int upgradeTimeout;
@Value("${socketio.pingTimeout}")
private int pingTimeout;
@Value("${socketio.pingInterval}")
private int pingInterval;
/**
* 以下配置在上面的application.yml中已经注明
* @return 实例化socketIo的服务对象
*/
@Bean
public SocketIOServer socketIOServer() {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setTcpNoDelay(true);
socketConfig.setSoLinger(0);
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
config.setHostname(host);
config.setPort(port);
config.setBossThreads(bossCount);
config.setWorkerThreads(workCount);
config.setAllowCustomRequests(allowCustomRequests);
config.setUpgradeTimeout(upgradeTimeout);
config.setPingTimeout(pingTimeout);
config.setPingInterval(pingInterval);
return new SocketIOServer(config);
}
}
application-dev.yml增加以下配置
#============================================================================
# netty socket io setting
#============================================================================
# host在本地测试可以设置为localhost或者本机IP,在Linux服务器跑可换成服务器IP
socketio:
host: localhost
port: 9099
# 设置最大每帧处理数据的长度,防止他人利用大数据来***服务器
maxFramePayloadLength: 1048576
# 设置http交互最大内容长度
maxHttpContentLength: 1048576
# socket连接数大小(如只监听一个端口boss线程组为1即可)
bossCount: 1
workCount: 100
allowCustomRequests: true
# 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
upgradeTimeout: 1000000
# Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
pingTimeout: 6000000
# Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
pingInterval: 25000
SocketIoService
package com.github.bg.admin.core.config.socketio;
public interface SocketIoService {
/**
* 推送的事件
*/
String PUSH_EVENT = "push_event";
/**
* 启动服务
*
* @throws Exception
*/
void start() throws Exception;
/**
* 停止服务
*/
void stop();
/**
* 功能描述:实现消息的发送
*
* @param loginAccount 需要发送到的账号
* @param content 需要发送的内容
*/
void pushMessage(String loginAccount, String content);
}
SocketIoService的实现SocketIoServiceImpl
在socketio包底下创建一个impl包同时创建SocketIoServiceImpl.java代码如下:
package com.github.bg.admin.core.config.socketio.impl;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.github.bg.admin.core.config.socketio.PushMessage;
import com.github.bg.admin.core.config.socketio.SocketIoService;
import com.github.bg.admin.core.util.RedisCache;
import com.github.bg.admin.core.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author linzf
* @since 2019-06-13
* 类描述:socket的实现类
*/
@Service(value = "socketIOService")
public class SocketIoServiceImpl implements SocketIoService {
private static Logger log = LoggerFactory.getLogger(SocketIoServiceImpl.class);
/**
* 用来存已连接的客户端
*/
private static Map<String, SocketIOClient> clientMap = new ConcurrentHashMap<>();
@Autowired
private SocketIOServer socketIOServer;
@Autowired
private RedisCache redisCache;
/**
* Spring IoC容器创建之后,在加载SocketIOServiceImpl Bean之后启动
*
* @throws Exception
*/
@PostConstruct
private void autoStartup() throws Exception {
start();
}
/**
* Spring IoC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
*
* @throws Exception
*/
@PreDestroy
private void autoStop() throws Exception {
stop();
}
/**
* 功能描述:启动socket服务
*/
@Override
public void start() {
// 监听客户端连接
socketIOServer.addConnectListener(client -> {
List<String> socketTokens = getParamsByClient(client).get("socketToken");
if (socketTokens.size() == 0) {
log.debug("socket连接失败,失败原因:socketToken的值不能为空!");
return;
}
List<String> refreshTokens = getParamsByClient(client).get("refreshToken");
if (refreshTokens.size() == 0) {
log.debug("socket连接失败,失败原因:refreshToken的值不能为空!");
return;
}
String refreshToken = refreshTokens.get(0);
String powerPath = redisCache.getString(refreshToken);
if (powerPath == null || "".equals(powerPath)) {
log.debug("socket连接失败,失败原因:无此token的用户权限信息!");
return;
}
String socketToken = socketTokens.get(0);
if (socketToken != null && !"".equals(socketToken)) {
clientMap.put(socketToken, client);
/**
* 防止不断刷新页面导致不断的增加socket的连接
*/
String socketTokenOld = redisCache.getString(refreshToken + "_SOCKET");
if (socketTokenOld != null && !"".equals(socketTokenOld)) {
SocketIOClient socketIOClient = clientMap.get(socketTokenOld);
if(socketIOClient!=null){
clientMap.get(socketTokenOld).disconnect();
clientMap.remove(socketTokenOld);
}
}
log.debug("当前保持连接的socket的数为{}", clientMap.size());
/**
* 与socket连接成功,将用户和socket的绑定关系保存到redis中
*/
redisCache.setString(refreshToken + "_SOCKET", socketToken);
}
});
// 监听客户端断开连接
socketIOServer.addDisconnectListener(client -> {
List<String> socketTokens = getParamsByClient(client).get("socketToken");
if (socketTokens.size() == 0) {
log.debug("socket连接失败,失败原因:socketToken的值不能为空!");
return;
}
String socketToken = socketTokens.get(0);
if (socketToken != null && !"".equals(socketToken)) {
clientMap.remove(socketToken);
client.disconnect();
}
});
// 处理自定义的事件,与连接监听类似
socketIOServer.addEventListener(PUSH_EVENT, PushMessage.class, (client, data, ackSender) -> {
// TODO do something
});
socketIOServer.start();
}
/**
* 功能描述:关闭socket服务
*/
@Override
public void stop() {
if (socketIOServer != null) {
socketIOServer.stop();
socketIOServer = null;
}
}
/**
* 功能描述:实现消息的发送
*
* @param loginAccount 需要发送到的账号
* @param content 需要发送的内容
*/
@Override
public void pushMessage(String loginAccount, String content) {
/**
* 功能描述:获取当前在线的用户的token和refreshToken
*/
List<String> loginTokens = redisCache.queryKeys(loginAccount);
for (String loginToken : loginTokens) {
if (!"".equals(loginToken)) {
// 获取当前的refreshToken的值
String loginRefreshTokenValue = loginToken.split(":")[1];
// 通过refreshToken来获取socketToken的值
String socketToken = redisCache.getString(loginRefreshTokenValue + "_SOCKET");
if (StringUtils.isNotEmpty(socketToken)) {
SocketIOClient client = clientMap.get(socketToken);
if (client != null) {
client.sendEvent(PUSH_EVENT, new PushMessage(socketToken, content));
}
}
}
}
}
/**
* 此方法为获取client连接中的参数,可根据需求更改
*
* @param client
* @return
*/
private Map<String, List<String>> getParamsByClient(SocketIOClient client) {
// 从请求的连接中拿出参数
Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
return params;
}
}
UserInfo工具类的实现
package com.github.bg.admin.core.util;
import com.github.bg.admin.core.entity.User;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author linzf
* @since 2019/6/21
* 类描述:用户工具类
*/
public class UserInfo {
/**
* 功能描述:获取当前登录的用户的信息
* @param redisCache
* @return
*/
public static User getLoginUser(RedisCache redisCache){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("x-access-token");
if("".equals(token)){
return null;
}
return redisCache.getObject(token + "_USER",User.class);
}
}
执行以下脚本创建消息相关表
CREATE TABLE `t_message` (
`messageId` varchar(32) NOT NULL COMMENT '消息流水ID',
`title` varchar(100) DEFAULT NULL COMMENT '消息标题',
`content` varchar(500) DEFAULT NULL COMMENT '消息内容',
`crtDate` datetime DEFAULT NULL COMMENT '创建事件',
`crtUserId` varchar(32) DEFAULT NULL COMMENT '创建用户ID',
`crtUserName` varchar(100) DEFAULT NULL COMMENT '创建用户名称',
`type` varchar(2) DEFAULT NULL COMMENT '消息类型【1:系统消息;2:其他消息】',
PRIMARY KEY (`messageId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用于存放主体的消息信息';
CREATE TABLE `t_target_message` (
`targetMessageId` varchar(32) NOT NULL COMMENT '流水ID',
`messageId` varchar(32) DEFAULT NULL COMMENT '消息流水ID',
`state` varchar(2) DEFAULT NULL COMMENT '消息状态【1:未读;2:已读】',
`userId` varchar(32) DEFAULT NULL COMMENT '用户ID',
`sendTime` datetime DEFAULT NULL COMMENT '发送时间',
`readeTime` datetime DEFAULT NULL COMMENT '阅读时间',
`removeState` varchar(2) DEFAULT NULL COMMENT '删除状态【0:已删除;1:正常】',
PRIMARY KEY (`targetMessageId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用于接收消息的表';
通过tk.mybatis工具生成实体和代码
打开generatorConfig.xml修改成如下代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<!-- 是否区分大小写,默认值 false -->
<property name="caseSensitive" value="true"/>
<!-- 是否强制生成注解,默认 false,如果设置为 true,不管数据库名和字段名是否一致,都会生成注解(包含 @Table 和 @Column) -->
<property name="forceAnnotation" value="true"/>
</plugin>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/vcm?characterEncoding=utf-8"
userId="root"
password="123456">
</jdbcConnection>
<javaModelGenerator targetPackage="com.github.bg.admin.core.entity" targetProject="src/main/java">
</javaModelGenerator>
<sqlMapGenerator targetPackage="mybatis/mapper" targetProject="src/main/resources" />
<javaClientGenerator targetPackage="com.github.bg.admin.core.dao" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_message" domainObjectName="Message" mapperName="MessageDao">
<!-- 字段属性是否驼峰展示,true为驼峰展示 -->
<property name="useActualColumnNames" value="true"></property>
<generatedKey column="messageId" sqlStatement="JDBC"/>
</table>
<table tableName="t_target_message" domainObjectName="TargetMessage" mapperName="TargetMessageDao">
<!-- 字段属性是否驼峰展示,true为驼峰展示 -->
<property name="useActualColumnNames" value="true"></property>
<generatedKey column="targetMessageId" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
然后在执行插件的方法如下
引入socket.io的maven依赖
在pom.xml中引入socket.io的依赖。
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.7</version>
</dependency>
Message实体改造
@Table(name = "t_message")
public class Message {
/**
* 消息流水ID
*/
@Id
@Column(name = "messageId")
@KeySql(genId = UuidGenId.class)
private String messageId;
/**
* 消息标题
*/
@Column(name = "title")
private String title;
/**
* 消息内容
*/
@Column(name = "content")
private String content;
/**
* 创建事件
*/
@Column(name = "crtDate")
private Date crtDate;
/**
* 创建用户ID
*/
@Column(name = "crtUserId")
private String crtUserId;
/**
* 创建用户名称
*/
@Column(name = "crtUserName")
private String crtUserName;
/**
* 消息类型【1:系统消息;2:其他消息】
*/
@Column(name = "type")
private String type;
/**
* 消息的接受者集合
*/
@Transient
private List<TargetMessage> targetMessageList;
// 省略get和set
}
TargetMessage改造
@Table(name = "t_target_message")
public class TargetMessage {
/**
* 流水ID
*/
@Id
@Column(name = "targetMessageId")
@KeySql(genId = UuidGenId.class)
private String targetMessageId;
/**
* 消息流水ID
*/
@Column(name = "messageId")
private String messageId;
/**
* 消息状态【1:未读;2:已读】
*/
@Column(name = "state")
private String state;
/**
* 用户ID
*/
@Column(name = "userId")
private String userId;
/**
* 接收人姓名
*/
@Column(name = "targetName")
private String targetName;
/**
* 发送时间
*/
@Column(name = "sendTime")
private Date sendTime;
/**
* 阅读时间
*/
@Column(name = "readeTime")
private Date readeTime;
/**
* 删除状态【0:已删除;1:正常】
*/
@Column(name = "removeState")
private String removeState;
// 省略set和get
}
MessageDao.xml改造
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.bg.admin.core.dao.MessageDao">
<resultMap id="BaseResultMap" type="com.github.bg.admin.core.entity.Message">
<id column="messageId" jdbcType="VARCHAR" property="messageId"/>
<result column="title" jdbcType="VARCHAR" property="title"/>
<result column="content" jdbcType="VARCHAR" property="content"/>
<result column="crtDate" jdbcType="TIMESTAMP" property="crtDate"/>
<result column="crtUserId" jdbcType="VARCHAR" property="crtUserId"/>
<result column="crtUserName" jdbcType="VARCHAR" property="crtUserName"/>
<result column="type" jdbcType="VARCHAR" property="type"/>
</resultMap>
<resultMap id="queryResultMap" type="com.github.bg.admin.core.entity.Message">
<id column="messageId" jdbcType="VARCHAR" property="messageId"/>
<result column="title" jdbcType="VARCHAR" property="title"/>
<result column="content" jdbcType="VARCHAR" property="content"/>
<result column="crtDate" jdbcType="TIMESTAMP" property="crtDate"/>
<result column="crtUserId" jdbcType="VARCHAR" property="crtUserId"/>
<result column="crtUserName" jdbcType="VARCHAR" property="crtUserName"/>
<result column="type" jdbcType="VARCHAR" property="type"/>
<collection property="targetMessageList" ofType="com.github.bg.admin.core.entity.TargetMessage"
javaType="java.util.ArrayList">
<id column="targetMessageId" jdbcType="VARCHAR" property="targetMessageId"/>
<result column="state" jdbcType="VARCHAR" property="state"/>
<result column="userId" jdbcType="VARCHAR" property="userId"/>
<result column="targetName" jdbcType="VARCHAR" property="targetName"/>
<result column="sendTime" jdbcType="TIMESTAMP" property="sendTime"/>
<result column="readeTime" jdbcType="TIMESTAMP" property="readeTime"/>
<result column="removeState" jdbcType="VARCHAR" property="removeState"/>
</collection>
</resultMap>
<!-- 获取当前登录的用户的未读的消息 -->
<select id="queryUserMsg" resultMap="queryResultMap">
select tm.*,ttm.* from t_message tm inner join t_target_message ttm on tm.messageId = ttm.messageId where
ttm.state='1'
<if test="userId!=null and userId!=''">
and ttm.userId = #{userId}
</if>
</select>
<!-- 查询消息列表的数据 -->
<select id="queryMessageList" resultMap="queryResultMap">
select tm.*,ttm.* from t_message tm inner join t_target_message ttm on tm.messageId = ttm.messageId where 1=1
<if test="search != null and search != ''">
and (
tm.title like concat('%',#{search},'%') or
tm.content like concat('%',#{search},'%')
)
</if>
</select>
</mapper>
MessageDao改造
package com.github.bg.admin.core.dao;
import com.github.bg.admin.core.entity.Message;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
public interface MessageDao extends Mapper<Message> {
/**
* 功能描述:获取当前登录的用户的未读的消息
* @param userId 用户ID
* @return 返回消息数据
*/
List<Message> queryUserMsg(@Param("userId")String userId);
/**
* 功能描述:查询消息数据
* @param search 匹配的内容
* @return 返回查询的结果
*/
List<Message> queryMessageList(@Param("search")String search);
}
TargetMessageDao.xml改造
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.bg.admin.core.dao.TargetMessageDao">
<resultMap id="BaseResultMap" type="com.github.bg.admin.core.entity.TargetMessage">
<id column="targetMessageId" jdbcType="VARCHAR" property="targetMessageId" />
<result column="messageId" jdbcType="VARCHAR" property="messageId" />
<result column="state" jdbcType="VARCHAR" property="state" />
<result column="userId" jdbcType="VARCHAR" property="userId" />
<result column="sendTime" jdbcType="TIMESTAMP" property="sendTime" />
<result column="targetName" jdbcType="VARCHAR" property="targetName" />
<result column="readeTime" jdbcType="TIMESTAMP" property="readeTime" />
<result column="removeState" jdbcType="VARCHAR" property="removeState" />
</resultMap>
<!-- 阅读消息以后的处理 -->
<update id="readMsg" >
update t_target_message set state = #{state} , readeTime = #{readeTime} where targetMessageId = #{targetMessageId}
</update>
</mapper>
TargetMessageDao改造
package com.github.bg.admin.core.dao;
import com.github.bg.admin.core.entity.TargetMessage;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
import java.util.Date;
public interface TargetMessageDao extends Mapper<TargetMessage> {
/**
* 功能描述:实现消息的阅读
* @param state 消息的状态
* @param readeTime 阅读时间
* @param targetMessageId 消息关联流水ID
* @return 返回更新结果
*/
int readMsg(@Param("state") String state,
@Param("readeTime") Date readeTime,
@Param("targetMessageId") String targetMessageId);
}
MessageService实现
package com.github.bg.admin.core.service;
import com.github.bg.admin.core.config.socketio.SocketIoService;
import com.github.bg.admin.core.constant.MessageConstant;
import com.github.bg.admin.core.constant.SystemStaticConst;
import com.github.bg.admin.core.dao.MessageDao;
import com.github.bg.admin.core.dao.TargetMessageDao;
import com.github.bg.admin.core.entity.*;
import com.github.bg.admin.core.util.PageUtil;
import com.github.bg.admin.core.util.RedisCache;
import com.github.bg.admin.core.util.UserInfo;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* @author linzf
* @since 2019/6/19
* 类描述:消息的service
*/
@Service
@Transactional(rollbackFor = {IllegalArgumentException.class})
public class MessageService {
/**
* 消息的dao
*/
@Autowired
private MessageDao messageDao;
/**
* 接收的消息的dao
*/
@Autowired
private TargetMessageDao targetMessageDao;
/**
* socket 推送的bean
*/
@Autowired
private SocketIoService socketIoService;
/**
* redis工具类的bean
*/
@Autowired
private RedisCache redisCache;
/**
* 功能描述:实现消息的阅读
*
* @param targetMessageId 消息关联流水ID
* @return 返回更新结果
*/
public ReturnInfo readMsg(String targetMessageId) {
if (targetMessageDao.readMsg(MessageConstant.READE_STATE_READE, new Date(), targetMessageId) == 0) {
return new ReturnInfo(SystemStaticConst.FAIL, "消息阅读失败!");
} else {
return new ReturnInfo(SystemStaticConst.SUCCESS, "消息阅读成功!");
}
}
/**
* 功能描述:获取当前登录的用户的未读的消息
*
* @return 返回消息数据
*/
public ReturnInfo queryUserMsg() {
User user = UserInfo.getLoginUser(redisCache);
if (user == null) {
return new ReturnInfo(SystemStaticConst.FAIL, "用户未登录!");
}
return new ReturnInfo(SystemStaticConst.SUCCESS, "获取用户消息成功!", messageDao.queryUserMsg(user.getUserId()));
}
/**
* 功能描述:实现消息的发布
*
* @param title 消息标题
* @param content 消息内容
* @param targetUsers 接收用户ID
* @return 返回发布结果
*/
public ReturnInfo publishNews(String title, String content, String[] targetUsers) {
User user = UserInfo.getLoginUser(redisCache);
if (user == null) {
return new ReturnInfo(SystemStaticConst.FAIL, "用户未登录!");
}
Message message = new Message();
message.setTitle(title);
message.setContent(content);
message.setCrtDate(new Date());
message.setCrtUserId(user.getUserId());
message.setCrtUserName(user.getNickName());
message.setType(MessageConstant.MESSAGE_TYPE_SYSTEM);
messageDao.insert(message);
String[] targetUserSplit;
TargetMessage targetMessage;
// 插入接收者的数据
for (String targetUser : targetUsers) {
targetUserSplit = targetUser.split("\\|");
targetMessage = new TargetMessage();
targetMessage.setMessageId(message.getMessageId());
targetMessage.setTargetName(user.getNickName());
targetMessage.setRemoveState(MessageConstant.REMOVE_STATE_NORMAL);
targetMessage.setUserId(targetUserSplit[1]);
targetMessage.setState(MessageConstant.READE_STATE_NOT_READE);
targetMessage.setSendTime(new Date());
targetMessageDao.insert(targetMessage);
socketIoService.pushMessage(targetUserSplit[0], content);
}
return new ReturnInfo(SystemStaticConst.SUCCESS, "消息发布成功");
}
/**
* 功能描述:获取消息列表
*
* @param search 模糊匹配消息的title和content
* @param pageSize 每页显示的记录的条数
* @param current 当前访问第几页
* @param orderKey 排序字段
* @param orderByValue 排序方式,降序还是升序
* @return 返回查询结果
*/
public ReturnInfo queryMessageList(String search, int pageSize, int current, String orderKey, String orderByValue) {
PageHelper.startPage(current, (pageSize > 0 && pageSize <= 500) ? pageSize : 20, (orderKey != null && !"".equals(orderKey)) ? ((orderByValue != null && !"".equals(orderByValue)) ? (orderKey + " " + orderByValue) : orderKey) : "");
HashMap<String, Object> res = PageUtil.getResult(messageDao.queryMessageList(search));
return new ReturnInfo(SystemStaticConst.SUCCESS, "获取消息列表数据成功!", new Page(pageSize, current, (long) res.get("total"), (List) res.get("rows")));
}
}
MessageController的实现
package com.github.bg.admin.core.controller;
import com.github.bg.admin.core.entity.ReturnInfo;
import com.github.bg.admin.core.service.MessageService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author linzf
* @since 2019/6/19
* 类描述:消息的controller
*/
@RestController
@RequestMapping("/msg")
public class MessageController {
@Autowired
private MessageService messageService;
/**
* 功能描述:实现消息的阅读
* @param targetMessageId 消息关联流水ID
* @return 返回更新结果
*/
@ApiOperation(value = "实现消息的阅读")
@PostMapping("readMsg")
public ReturnInfo readMsg(@RequestParam(name = "targetMessageId")String targetMessageId){
return messageService.readMsg(targetMessageId);
}
/**
* 功能描述:获取当前登录的用户的未读的消息
* @return 返回消息数据
*/
@ApiOperation(value = "获取当前登录的用户的未读的消息")
@PostMapping("queryUserMsg")
public ReturnInfo queryUserMsg(){
return messageService.queryUserMsg();
}
/**
* 功能描述:实现消息的发布
* @param title 消息标题
* @param content 消息内容
* @param targetUsers 接收用户ID
* @return 返回发布结果
*/
@ApiOperation(value = "消息的发布")
@PostMapping("publishNews")
public ReturnInfo publishNews(@RequestParam(name = "title")String title,
@RequestParam(name = "content")String content,
@RequestParam(name = "targetUsers")String [] targetUsers){
return messageService.publishNews(title,content,targetUsers);
}
/**
* 功能描述:获取消息列表
*
* @param search 模糊匹配消息的title和content
* @param pageSize 每页显示的记录的条数
* @param current 当前访问第几页
* @param orderKey 排序字段
* @param orderByValue 排序方式,降序还是升序
* @return 返回查询结果
*/
@ApiOperation(value = "获取消息列表")
@PostMapping("queryMessageList")
public ReturnInfo queryMessageList(@RequestParam(name = "search", required = false) String search,
@RequestParam(name = "pageSize") int pageSize,
@RequestParam(name = "current") int current,
@RequestParam(name = "orderKey", required = false) String orderKey,
@RequestParam(name = "orderByValue", required = false) String orderByValue) {
return messageService.queryMessageList(search, pageSize, current, orderKey, orderByValue);
}
}
前端改造
user.js改造
store/modules底下的user.js的handleLogin方法进行改造,改造完成以后的代码如下:
handleLogin ({ commit }, {loginAccount, loginPassword}) {
loginAccount = loginAccount.trim();
return new Promise((resolve, reject) => {
login({
loginAccount,
loginPassword
}).then(res => {
if(res.code!=200){
commit('setMsg', res.msg);
}else{
localStorage.setItem('token', res.obj.token);
localStorage.setItem('refreshToken', res.obj.refreshToken);
commit('setToken', res.obj.token);
commit('setRefreshToken', res.obj.refreshToken);
}
resolve(res);
}).catch(err => {
reject(err);
});
})
}
引入socket.io-client
在package.json的dependencies底下加入以下的依赖:
"socket.io-client": "^2.2.0"
接着下载依赖,执行以下代码
cnpm install
编写生成uuid的方法
在main.js中加入以下的代码,实现全局可用。
/**
* 生成唯一的uuid
* @returns {string}
*/
Vue.prototype.$uuid = function(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
添加msg的api
在api目录底下创建一个msg文件夹,接着在该文件夹底下创建一个msg.api.js,代码如下:
import {fetch} from '../../base';
// 实现消息的阅读
export const readMsg = params => {
return fetch('/msg/readMsg',params);
};
// 获取当前登录的用户的未读的消息
export const queryUserMsg = params => {
return fetch('/msg/queryUserMsg',params);
};
// 发布消息
export const publishNews = params => {
return fetch('/msg/publishNews',params);
};
// 获取消息列表
export const queryMessageList = params => {
return fetch('/msg/queryMessageList',params);
};
main.vue改造
<template>
<div class="layout">
<Layout>
<!-- Header 表示头部的位置-->
<Header id="layout-header-scroll">
<Menu mode="horizontal" theme="dark" active-name="1">
<div class="layout-logo">
<img height="50px" width="50px" src="../../assets/logo.png"/>
</div>
<div class="layout-nav" >
<Poptip
placement="bottom-end"
:transfer=true
ref="queryMsgPop"
>
<Badge dot :offset="[21,21]" :count="msgCount">
<a href="javascript:void(0)" >
<Icon type="ios-chatbubbles-outline" size="36"></Icon>
</a>
</Badge>
<div slot="content">
<template v-for="(msg,index) in msgList">
<Divider v-if="index==0" />
<p @click="showMsg(msg.title,msg.content,index,msg.targetMessageList[0].targetMessageId)">{{msg.content}}</p>
<Divider />
</template>
</div>
</Poptip>
</div>
<div class="layout-nav">
<Dropdown @on-click="userAction">
<a href="javascript:void(0)" style="color: white">
{{this.nickName}}
</a>
<DropdownMenu slot="list">
<DropdownItem name="regPass">修改密码</DropdownItem>
<DropdownItem name="loginOut" divided>退出登录</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<div class="layout-nav">
<language @on-lang-change="setLanguage" style="margin-right: 10px;" :lang="local"/>
</div>
<div class="layout-nav">
<template v-for="item in menuList">
<Submenu :name="item.meta.code" v-if="item.children.length>0">
<template slot="title">
<Icon :type="item.meta.icon"/>
{{item.meta.title}}
</template>
<template v-for="childrenItem in item.children">
<MenuItem :name=childrenItem.meta.code :to="item.path+'/'+childrenItem.path">
<Icon :type=childrenItem.meta.icon>
</Icon>
{{childrenItem.meta.title}}
</MenuItem>
</template>
</Submenu>
<MenuItem :name="item.meta.code" v-else>
<Icon :type="item.meta.icon"/>
{{item.meta.title}}
</MenuItem>
</template>
</div>
</Menu>
</Header>
<!-- 此处表示的是左侧的菜单栏的布局 -->
<Layout>
<Sider hide-trigger :style="{background: '#fff'}">
<Menu active-name="1-2" theme="light" width="auto" :open-names="['system-manage']">
<template v-for="item in menuList">
<Submenu :name=item.meta.code>
<template slot="title">
<Icon :type=item.meta.icon></Icon>
{{item.meta.title}}
</template>
<template v-for="childrenItem in item.children">
<MenuItem :name=childrenItem.meta.code :to="item.path+'/'+childrenItem.path">
<Icon :type=childrenItem.meta.icon>
</Icon>
{{childrenItem.meta.title}}
</MenuItem>
</template>
</Submenu>
</template>
</Menu>
</Sider>
<Layout :style="{padding: '0 24px 24px'}">
<!-- 此处是面包屑导航条 -->
<Breadcrumb :style="{margin: '24px 0'}">
<BreadcrumbItem>
<Icon type="ios-home-outline"></Icon>
首页
</BreadcrumbItem>
<BreadcrumbItem v-for="item in breadCrumbList" v-bind:key="item.name" v-if="item.meta && item.meta.title">
<Icon :type="item.icon"></Icon>
{{showBreadcrumbItem(item)}}
</BreadcrumbItem>
</Breadcrumb>
<!-- 此处存放的是文本内容的区域 -->
<Content :style="{padding: '24px', minHeight: '280px', background: '#fff'}">
<router-view/>
</Content>
</Layout>
</Layout>
</Layout>
<changePassword v-model="showChangePassword"></changePassword>
</div>
</template>
<script>
import Language from '../../components/language';
import {mapMutations, mapActions} from 'vuex';
import changePassword from './changePassword';
import {queryUserMsg,readMsg} from '../../api/sys/msg/msg.api';
import io from 'socket.io-client'
export default {
components: {
Language,
changePassword
},
data() {
return {
local: localStorage.getItem("lang"),
showChangePassword: false,
msgCount: 0,
msgList:[]
}
},
methods: {
...mapMutations([
'setBreadCrumb'
]),
...mapActions([
'handleLogOut'
]),
/**
* 顶部跟随着滚动条的变化而滚动
*/
handleScroll() {
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
if (scrollTop >= 60) {
document.querySelector('#layout-header-scroll').style.top = scrollTop + 'px';
} else {
document.querySelector('#layout-header-scroll').style.top = '0px';
}
},
userAction(name) {
// 实现退出登录
if (name == 'loginOut') {
this.handleLogOut();
this.turnToView('login');
// 实现修改密码
} else if (name == 'regPass') {
this.showChangePassword = true;
}
},
setLanguage(lang) {
this.local = lang
localStorage.setItem('lang', lang)
},
showBreadcrumbItem(item) {
return (item.meta && item.meta.title) || item.name
},
turnToView(name) {
this.$router.push({
name: name
})
},
showMsg(title,content,index,targetMessageId){
readMsg({targetMessageId}).then(res=>{
if (res.code == 200) {
this.$Modal.info({title,content});
this.msgList.splice(index,index+1)
} else {
this.$Message.error(res.msg)
}
});
},
initMsg() {
queryUserMsg({}).then(res => {
if (res.code == 200) {
this.msgCount = res.obj.length;
this.msgList = res.obj;
} else {
this.$Message.error(res.msg)
}
})
},
initSocketIo(socketToken,refreshToken){
let _this = this;
let opts = {
query: 'refreshToken=' + refreshToken + '&socketToken=' + socketToken
};
let socket = io.connect(this.$runConfig.runConfig.socketUrl,opts);
socket.on('connect', function () {
console.log("连接成功");
});
socket.on('push_event', function (data) {
console.log(data);
_this.$Notice.info({
title: '消息通知',
desc: data.content
});
});
socket.on('disconnect', function () {
console.log("已经下线");
});
}
},
watch: {
'$route'(newRoute) {
this.setBreadCrumb(newRoute.matched)
}
},
computed: {
breadCrumbList() {
return this.$store.state.app.breadCrumbList
},
menuList() {
return this.$store.getters.menuList;
},
nickName() {
return this.$store.getters.nickName;
}
},
mounted() {
/**
* 监听滚动条的滚动事件
*/
window.addEventListener('scroll', this.handleScroll)
/**
* 开启socket的监听
*/
this.initSocketIo(this.$uuid(), localStorage.getItem("refreshToken"));
this.initMsg();
}
}
</script>
<style scoped>
.layout-header {
position: relative;
z-index: 999;
height: 60px;
}
.layout {
border: 1px solid #d7dde4;
background: #f5f7f9;
position: relative;
border-radius: 4px;
overflow: hidden;
}
.layout-logo {
width: 100px;
height: 30px;
border-radius: 10px;
float: left;
position: relative;
left: 20px;
top: 5px;
}
.layout-nav {
width: auto;
float: right;
margin: 0 auto;
margin-right: 20px;
}
</style>
消息模块的实现
在sys目录底下创建一个msg目录,分别实现以下的三个页面:
MsgList.vue【消息列表】
<template>
<div>
<Card title="消息管理">
<div>
<div style="display:inline-block;float:left;">
<Button type="success" @click="handleAdd">+发布消息</Button>
</div>
<div style="display:inline-block;float:right;">
<Input v-model="search" suffix="ios-search" placeholder="请输入相应的查询信息" style="width: auto"
:search=true @on-search="handleSearch"/>
</div>
</div>
<div style="clear: both;"></div>
<div style="margin-top: 10px;">
<Table ref="msgTable" @on-sort-change="onSortChange" :columns="columns" :data="msgData" :height="tableHeight">
</Table>
</div>
<div style="margin-top: 10px;">
<Page show-elevator show-sizer show-total :total="total" :current="current"
:page-size="pageSize" @on-change="changePage" @on-page-size-change="changePageSize"/>
</div>
</Card>
<publishNews v-model="addShow" v-on:handleSearch="handleSearch"></publishNews>
</div>
</template>
<script>
import {queryMessageList} from '../../../api/sys/msg/msg.api'
import expandReceiverList from './ExpandReceiverList'
import publishNews from './publishNews'
export default {
name: 'msgList',
components:{
expandReceiverList,
publishNews
},
data() {
return {
search: '',
addShow: false,
msgData: [],
tableHeight: 200,
key: 'crtDate',
order: 'desc',
total: 0,
current: 1,
pageSize: 10,
columns: [
{
type: 'expand',
width: 50,
render: (h, params) => {
return h(expandReceiverList, {
props: {
targetMessageList: params.row.targetMessageList
}
})
}
},
{
title: '消息标题',
key: 'title',
tooltip: true,
sortable: true
},
{
title: '消息内容',
key: 'content',
tooltip: true,
sortable: true
},
{
title: '消息类型',
key: 'type',
sortable: true,
render: (h, params) => {
if (params.row.type == '1') {
return h('div',
'系统消息'
)
} else {
return h('div',
'其他消息'
)
}
}
}
]
}
},
methods: {
changePage(current) {
this.current = current;
this.handleSearch();
},
changePageSize(pageSize) {// 改变每页记录的条数
this.pageSize = pageSize;
this.handleSearch();
},
onSortChange(sort) {
if (sort.order == 'normal') {
this.order = '';
} else {
this.key = sort.key;
this.order = sort.order;
}
this.handleSearch();
},
handleAdd(){
this.addShow=true
},
handleSearch() {
let current = this.current
let pageSize = this.pageSize
let search = this.search
let orderKey = this.key
let orderByValue = this.order
const _this = this;
queryMessageList({
current,
pageSize,
search,
orderKey,
orderByValue,
}).then(res => {
if (res.code == 200) {
this.$Message.success('数据查询成功!')
_this.total = res.obj.total
_this.msgData = res.obj.rows
} else {
this.$Message.error('数据查询失败,失败原因:' + res.msg)
}
});
}
},
mounted() {
// 初始化完成组件的时候执行以下的逻辑
this.handleSearch();
this.tableHeight = window.innerHeight - this.$refs.msgTable.$el.offsetTop - 270
}
}
</script>
publishNews.vue【实现消息的发送】
<template>
<Modal v-model="show" title="发布消息" @on-ok="ok" :loading="loading" :mask-closable="false">
<Form ref="messageForm" :model="messageForm" :rules="messageFormRule">
<FormItem label="消息标题" prop="title">
<Input type="text" :maxlength=50 v-model="messageForm.title" placeholder="请输入消息标题"/>
</FormItem>
<FormItem label="消息内容" prop="content">
<Input type="textarea" :rows="4" :maxlength=1000 v-model="messageForm.content" placeholder="请输入消息内容"/>
</FormItem>
<FormItem label="接收者" prop="targetUsers">
<Transfer
:data="userData"
:render-format="userRender"
:target-keys="messageForm.targetUsers"
filterable
@on-change="handleChange"></Transfer>
</FormItem>
</Form>
</Modal>
</template>
<script>
import {
queryUserList
} from "../../../api/sys/user/user.api"
import {
publishNews
} from '../../../api/sys/msg/msg.api'
export default {
name: 'publishNews',
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
show: this.value,
loading: true,
userData: [],
messageForm: {
title: '',
content: '',
targetUsers: []
},
messageFormRule: {
title: [
{required: true, message: '消息标题不能为空!', trigger: 'blur'},
{type: 'string', max: 50, message: '消息标题最大长度不能超过50!', trigger: 'blur'}
],
content: [
{required: true, message: '消息内容不能为空!', trigger: 'blur'},
{type: 'string', max: 200, message: '消息内容长度不能超过200!', trigger: 'blur'}
],
targetUsers: [
{required: true, message: '接收者不能为空!', trigger: 'change', type: "array"}
]
}
}
},
methods: {
userRender(item) {
return item.label
},
handleChange(newTargetKeys, direction, moveKeys) {
this.messageForm.targetUsers = newTargetKeys;
},
ok() {
this.$refs['messageForm'].validate((valid) => {
if (valid) {
publishNews({
title: this.messageForm.title,
content: this.messageForm.content,
targetUsers: this.messageForm.targetUsers.join(",")
}).then(res => {
if (res.code == 200) {
this.$Message.success(res.msg);
// 提交表单数据成功则关闭当前的modal框
this.closeModal(false);
// 同时调用父页面的刷新页面的方法
this.$emit('handleSearch');
} else {
this.$Message.error(res.msg);
}
})
}
setTimeout(() => {
this.loading = false;
this.$nextTick(() => {
this.loading = true;
});
}, 1000);
})
},
closeModal(val) {
this.$emit('input', val);
},
initUserData() {
queryUserList({
current: 1,
pageSize: 9999,
search: '',
orderKey: '',
orderByValue: '',
fullPath: ''
}).then(res => {
if (res.code == 200) {
for (let i = 0; i < res.obj.rows.length; i++) {
let user = res.obj.rows[i];
user.label = user.loginAccount;
user.key = user.loginAccount + '|' + user.userId;
this.userData.push(user);
}
}
})
}
},
watch: {
value(val) {
this.show = val;
},
show(val) {
//当重新显示增加数据的时候重置整个form表单
if (val) {
this.$refs['messageForm'].resetFields();
} else {// 反之则关闭页面
this.closeModal(val);
}
}
},
mounted() {
this.initUserData()
}
}
</script>
ExpandReceiverList.vue【消息列表中消息的具体接收者的信息】
<template>
<Row>
<Col span="24">
<Table :columns="expandColumns" :data="expandData" stripe border >
</Table>
</Col>
</Row>
</template>
<script>
export default {
name: 'expandReceiveList',
props: {
targetMessageList: {
type: Array,
default: []
},
},
data() {
return {
expandData: this.targetMessageList,
expandColumns: [
{
title: '收取人',
key: 'targetName'
},
{
title: '阅读状态',
key: 'state',
render: (h, params) => {
if (params.row.state == '1') {
return h('div',
'未读'
)
} else {
return h('div',
'已读'
)
}
}
},
{
title: '发送时间',
key: 'sendTime',
render: (h, params) => {
if(params.row.sendTime){
return h('div',
this.formatDate(new Date(params.row.sendTime), 'yyyy/MM/dd hh:mm:ss')
)
}else{
return h('div',
''
)
}
}
},
{
title: '查看时间',
key: 'readeTime',
render: (h, params) => {
if(params.row.readeTime){
return h('div',
this.formatDate(new Date(params.row.readeTime), 'yyyy/MM/dd hh:mm:ss')
)
}else{
return h('div',
''
)
}
}
},
{
title: '是否删除',
key: 'removeState',
render: (h, params) => {
if (params.row.removeState == '0') {
return h('div',
'已删除'
)
} else {
return h('div',
'未删除'
)
}
}
}
]
}
}
}
</script>
增加router的配置
{
path: 'msgList',
name: 'msgList',
meta: {
icon: 'ios-chatbubbles',
title: '消息管理',
code:'system-manage-message',
requireAuth: true //表示当前响应的请求是否需要进行登录拦截验证【true:需要;false:不需要】
},
component: resolve => {
require(['../view/sys/msg/msgList.vue'], resolve);
}
}
运行项目
我们将整个项目运行起来,然后直接访问到我们的消息管理模块,我们会看到如下的页面:
接着我们发送一条消息会看到如下的效果则说明我们的消息模块已经完成了。
上一篇文章地址:spring boot+iview 前后端分离架构之登陆密码RSA加密(三十二)
下一篇文章地址:spring boot+iview 前后端分离架构之行为日志的实现(三十四)