赞
踩
聊天,即前端与后端通信,基于 websocket 通道,前端可以实时从 websocket 通道来获取后台推送的消息,就不需要刷新网页了。
pom加入websocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置文件
1 WebSocketConfig
// 1.WebSocketConfig package com.partner.boot.common; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration // 配置文件必备 @EnableWebSocket // 开启服务 public class WebSocketConfig { /** * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint * ServerEndpointExporter 是依赖所提供的的类 */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
2 WebSocketServer.java
package com.partner.boot.service.impl; import cn.hutool.core.lang.Dict; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.partner.boot.entity.Im; import com.partner.boot.entity.User; import com.partner.boot.service.IImService; import com.partner.boot.service.IUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.time.LocalDateTime; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author websocket服务 */ @ServerEndpoint(value = "/imserver/{uid}") // 定义路由 websocket 也需要路由 @Component // 注册为springbootservice 服务才能生效 public class WebSocketServer { private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class); public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>(); // 记录当前在线连接数 @Resource IUserService userService; @Resource IImService imService; private static IUserService staticUserService; private static IImService staticImService; /** * 0.页面初始化 * 程序初始化的时候触发这个方法 赋值 */ @PostConstruct public void setStaticUser() { // 在程序初始化时把 加载到内存,变成静态的成员变量 // 也可以通过 SpringBeanUtil ,但需要自己封装spring获取bean的类 staticUserService = userService; staticImService = imService; } /** * 1.页面开启 * 连接建立成功调用的方法 可以获取前端页面的 session (一个客户端和一个服务端建立的连接) 通过session往客户端发消息 * 当新的用户连接成功后,会广播给每一个用户当前有多少个人 * 并把新用户的 session 存到 sessionMap */ @OnOpen public void onOpen(Session session, @PathParam("uid") String uid) { // uid 作为key:一个用户只有一个客户端,只有一个页面。 sessionMap.put(uid, session); log.info("有新用户加入,uid={}, 当前在线人数为:{}", uid, sessionMap.size()); // 将当前所有用户个数传给客户端。Dict,Hutool 提供的 字典。 Dict dict = Dict.create().set("nums", sessionMap.size()); // 后台发送消息给所有的客户端 sendAllMessage(JSONUtil.toJsonStr(dict)); } /** * 2.页面关闭 * 连接关闭调用的方法 * 刷新(session会变),离开都算关闭 */ @OnClose public void onClose(Session session, @PathParam("uid") String uid) { // 从后台缓存去掉 sessionMap.remove(uid); log.info("有一连接关闭,uid={}的用户session, 当前在线人数为:{}", uid, sessionMap.size()); Dict dict = Dict.create().set("nums", sessionMap.size()); // 后台发送消息给所有的客户端 sendAllMessage(JSONUtil.toJsonStr(dict)); } /** * 3. 发送消息。核心功能。 * 收到客户端消息后调用的方法 * 后台收到客户端发送过来的消息 * onMessage 是一个消息的中转站 * 接受 浏览器端 socket.send 发送过来的 json数据 * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session fromSession, @PathParam("uid") String uid) throws JsonProcessingException { log.info("服务端收到用户uid={}的消息:{}:", uid, message); if (staticUserService == null) { return; } // 查询用户 User user = staticUserService.getOne(new QueryWrapper<User>().eq("uid", uid)); if (user == null) { log.error("获取用户信息失败,uid={}", uid); return; } // 4 // 直接存到数据库里面 Im im = Im.builder().uid(uid).username(user.getNamex()).avatar(user.getAvatar()).sign(user.getSign()) .createTime(LocalDateTime.now()).text(message).build(); // 2 // // 前端代码改了后 message 就不再是 json 了,而是消息字符串,需要自己构建DTO // ImMessageDTO messageDTO = ImMessageDTO.builder().uid(uid).username(user.getNamex()).avatar(user.getAvatar()).sign(user.getSign()) // .createTime(new Date()).text(message).build(); // 1 // // Json 字符串转化为数据类 // ImMessageDTO imMessageDTO = JSONUtil.toBean(message, ImMessageDTO.class); // imMessageDTO.setCreateTime(new Date()); // 3 // // 存数据到数据库 // Im im = new Im(); // BeanUtil.copyProperties(messageDTO,im); staticImService.save(im); // // message 处理好后再转回json;处理后的消息体; // 刚发消息时显示不出来,原因:实体类加了注解@Alias("xx"),json的key会变成中文 // String jsonStr = JSONUtil.toJsonStr(im); // 处理后的消息体 String jsonStr = new ObjectMapper().writeValueAsString(im); // 广播 // 消息体是后端构建的 前端需要使用数据时,前端自己不构建,依赖于后端发送的数据 // 所以自己也要接受到后端返回的消息 所以这里应该用 sendAllMessage 而不是 sendMessage this.sendAllMessage(jsonStr); log.info("发送消息:{}:", jsonStr); } /** * 3.1 * 广播 服务端发送消息给除了自己的其他客户端 */ private void sendMessage(Session fromSession, String message) { sessionMap.values().forEach(session -> { // 把发送消息的自己排除 if (fromSession != session) { log.info("服务端给客户端[{}]发送消息{}:", session.getId(), message); try { session.getBasicRemote().sendText(message); // 发消息 } catch (IOException e) { log.error("服务端发送消息给客户端异常", e); } } }); } /** * 3.2 * 服务端发送消息给所有客户端 */ private void sendAllMessage(String message) { try { for (Session session : sessionMap.values()) { log.info("服务端给客户端[{}]发送消息{}:", session.getId(), message); session.getBasicRemote().sendText(message); } } catch (Exception e) { log.error("服务端发送消息给客户端失败", e); } } /** * 4. * 发生错误 */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } }
安装表情包依赖 npm i vue3-emoji@1.3.0
表情包依赖文档:https://github.com/ADKcodeXD/Vue3-Emoji
im.vue
<script setup> import {nextTick, onMounted, ref} from "vue"; import V3Emoji from 'vue3-emoji' import 'vue3-emoji/dist/style.css' import {useUserStore} from "@/stores/user"; import request from "@/utils/request"; const messages = ref([]) const userStore = useUserStore() const user = userStore.getUser const text = ref('') // 聊天输入的内容 const divRef = ref() // 聊天框的引用。发送消息后,需要把滚动条滚动到最新位置,需要用到这个引用 // 页面滚动到最新位置的函数 const scrollBottom = () => { // 数据渲染通过v-if, scrollBottom函数触发时,页面的DOM不一定渲染好了 // 等到页面元素出来之后再去滚动 nextTick(() => { divRef.value.scrollTop = divRef.value.scrollHeight }) } // 页面加载完成触发此函数 onMounted(() => { // 数据加载完再滚动 request.get("/im/init/10").then(res => { messages.value = res.data // // 1.刷新滚动 // scrollBottom() }) }) // 后台 WebSocketServer.java 提供的路由 const client = new WebSocket(`ws://localhost:9090/imserver/${user.uid}`) // 2.发送消息触发滚动条滚动 const send = () => { if (client) { // 之前在前端写死数据 目的是 明白实体类应该包括哪些字段 // message.value.push({uid: user.id, username: user.name, avatar: user.avatar,text: text.value}) // 现在是改造的更简单 client.send(text.value) } text.value = '' // 清空文本框 // send 之后不能立马滚动,要接收到消息后再滚动 // 消息要转到后台存到数据库再发送给其它用户 // scrollBottom() } // 获取当前文件状态 client.onopen = () => { console.log('open') } // 页面刷新的时候 和 后台websocket服务关闭的时候 client.onclose = () => { console.log('close') } // 获取后台消息 client.onmessage = (msg) => { if (msg.data) { let json = JSON.parse(msg.data) // 有聊天消息 if (json.uid && json.text) { messages.value.push(json) // 消息接收到后,滚动页面到最底部 scrollBottom() } } } const optionsName = { 'Smileys & Emotion': '笑脸&表情', 'Food & Drink': '食物&饮料', 'Animals & Nature': '动物&自然', 'Travel & Places': '旅行&地点', 'People & Body': '人物&身体', Objects: '物品', Symbols: '符号', Flags: '旗帜', Activities: '活动' } </script> <template> <div style="width: 80%; margin: 10px auto"> <!-- 聊天框--> <div ref="divRef" style="background-color: white; padding: 20px; border: 1px solid #ccc; border-radius: 10px; height: 400px; overflow-y: scroll;"> <!-- 循环获取消息 --> <!-- item.uid + item.createTime + Math.random() 消息的唯一id --> <!-- 创建了im表后,表里的id是唯一的 --> <div v-for="item in messages" :key="item.id"> <!-- 1 别人给我发的消息 --> <div style="display: flex; margin: 20px 0;" v-if="user.uid !== item.uid"> <!-- 点击头像所显示的框框 --> <el-popover placement="top-start" :width="100" trigger="hover" > <!-- 头像 --> <template #reference> <img :src="item.avatar" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px"> </template> <!-- 弹出框类容 --> <div style="line-height: 20px"> <div style="font-size: 16px">{{ item.username }}</div> <div style="font-size: 12px;">{{ item.sign }}</div> </div> </el-popover> <!-- 聊天类容 width:fit-content 不加,消息会占满整行 --> <!-- <div style="width: 50px; line-height: 30px; margin-left: 5px; color: #888; overflow: hidden; font-size: 14px">{{ item.username }}</div> --> <div style="line-height: 30px; background-color: aliceblue; padding: 0 10px; width:fit-content; border-radius: 10px">{{ item.text }}</div> </div> <!-- 2 我给别人发的消息 justify-content: flex-end:靠右显示--> <div style="display: flex; justify-content: flex-end; margin: 20px 0;" v-else> <div style="line-height: 30px; background-color: lightyellow; padding: 0 10px; width:fit-content; border-radius: 10px;">{{ item.text }}</div> <el-popover placement="top-start" :width="100" trigger="hover" > <template #reference> <img :src="item.avatar" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-left: 10px"> </template> <div style="line-height: 20px"> <div style="font-size: 16px">{{ item.username }}</div> <div style="font-size: 12px;">{{ item.sign }}</div> </div> </el-popover> </div> </div> </div> <!-- 聊天输入框 第三方插件 --> <div style="margin: 10px 0; width: 100%"> <!-- :keep="true" 标签框关闭不会销毁组件。页面性能会好点,不用重复渲染。--> <!-- :textArea="true" 显示聊天框; v-model="text" 聊天框内容 --> <V3Emoji default-select="recent" :recent="true" :options-name="optionsName" :keep="true" :textArea="true" size="mid" v-model="text" /> <div style="text-align: right"><el-button @click="send" type="primary">发送</el-button></div> </div> </div> </template>
ImMessageDTO.java 后台接收前台的实体,作为数据传输用,本身没有太大价值,与数据库没关系。
发送的消息 --> 需要把数据发送给后台 --> 后端接收消息,存到数据库表 im 里去 im
表 --> ImMessageDTO就没用了。
创建im权限(页面)管理 、 为admin分配权限。
存储数据、取数据。
// ImController
@GetMapping("/init/{limit}")
// @PathVariable 接受花括号形式的参数
public Result findAllInit(@PathVariable Integer limit) {
// 默认取limit条数据
return Result.success(imService.list(new QueryWrapper<Im>().last("limit " + limit)));
}
问题:在 WebSocketServer.java 的 onMessage() 方法里 获取 user 失败
解决:
为什么消息会出现null?因为消息是多线程,前端每构建的消息通道,都是一个单独的线程,在多线程里面不能通过注入 @Resource IUserService userService;
从容器里拿到bean,可以通过 static 初始化进来,也可以通过 SpringBeanUtil ,但需要自己封装spring获取bean的类/方法,然后再从容器里拿bean。
// 把 userService 初始化进来
private static IUserService staticUserService;
/**
* 0.页面初始化
* 程序初始化的时候触发这个方法 赋值
*/
@PostConstruct
public void setStaticUser() {
staticUserService = userService;
}
换代码生成器了
增加 个人主页,修改密码,两个页面和后端接口。
前端:person.vue password.vue
后端:
// 1 WebController // 修改密码 @PostMapping("/password/change") public Result passwordChange(@RequestBody UserRequest userRequest) { userService.passwordChange(userRequest); return Result.success(); } // 更新个人信息 @PutMapping("/updateUser") public Result updateUser(@RequestBody User user) { Object loginId = StpUtil.getLoginId(); if (!loginId.equals(user.getUid())) { Result.error("无权限"); } userService.updateById(user); return Result.success(user); }
// 2 IUserService void passwordChange(UserRequest userRequest); // 3 UserRequest private String uid; private String newPassword; // 4 UserServiceImpl public void passwordChange(UserRequest userRequest) { User dbUser = getOne(new UpdateWrapper<User>().eq("uid", userRequest.getUid())); if (dbUser == null) { throw new ServiceException("未找到用户"); } boolean checkpw = BCrypt.checkpw(userRequest.getPassword(), dbUser.getPassword()); if (!checkpw) { throw new ServiceException("原密码错误"); } String newPass = userRequest.getNewPassword(); dbUser.setPassword(BCrypt.hashpw(newPass)); updateById(dbUser); // 设置到数据库 }
动态展示首页
在后台获取用户信息 User user = (User) StpUtil.getSession().get(Constants.LOGIN_USER_KEY);
// Dynamic
@TableField(exist = false)
private User user;
@GetMapping("/hot") @SaCheckPermission("dynamic.list.hot") public Result hot( @RequestParam Integer pageNum, @RequestParam Integer pageSize) { // 用户存在动态表里的是uid 这里获取信息时需要的是完整的用户id // 实际业务时需要在不同系统交互数据 这里的话单个系统比较简单 直接根据用户id查数据即可 // 但用户比较多,需要分页了,否则前端可能会卡了 QueryWrapper<Dynamic> queryWrapper = new QueryWrapper<Dynamic>().orderByDesc("id"); Page<Dynamic> page = dynamicService.page(new Page<>(pageNum, pageSize), queryWrapper); // 1 通过sql 2 直接从一个很大的用户接口里筛选需要的数据 // 这里使用 2 List<User> users = userService.list(); for (Dynamic record : page.getRecords()) { // 从users里面找到uid跟当前动态里面的uid一样的数据 // ifPresent 表示数据筛选有结果的时候 users.stream().filter(user -> user.getUid().equals(record.getUid())).findFirst().ifPresent(record::setUser); } return Result.success(page); }
前端
<script setup> import {ChatLineRound, Compass, Pointer, View} from '@element-plus/icons-vue' import request from "@/utils/request"; import {reactive} from "vue"; function filterTime(time) { const date = new Date(time) const Y = date.getFullYear() const M = date.getMonth() + 1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1 const D = date.getDate() return `${Y}-${M}-${D}` } const state = reactive({ hotDynamics: [] }) const load = () => { request.get('/dynamic/hot', { params: { pageNum: 1, pageSize: 5 } }).then(res => { state.hotDynamics = res.data.records console.log(res.data.records) }) } load() </script> <template> <div style="background-color: white; border-radius: 10px; margin-bottom: 10px" class="container-height;"> <div style="display: flex; padding: 10px"> <!-- 话题动态--> <div class="dynamic-box" style="width: 60%; "> <!-- :src="item.user.avatar" {{ item.user.namex }} --> <div style="padding: 20px; border: 1px solid #ccc; border-radius: 10px; margin-bottom: 10px" v-for="item in state.hotDynamics" :key="item.id"> <div style="display:flex;"> <img style="width: 50px; height: 50px; margin-right: 20px; border-radius: 50%" :src="item.user.avatar" alt=""> <div style="flex: 1; line-height: 25px"> <div style="font-weight: bold"> {{ item.user.namex }} </div> <div style="font-size: 12px; color: #999">{{ filterTime(item.createTime) }} · 来自 {{ item.user.address }}</div> </div> <el-button>关注</el-button> </div> <div style="" class="content">{{ item.description }}</div> <div style="margin: 10px 0"> <el-row :gutter="10"> <el-col :span="12" style="margin-bottom: 10px"> <img style="width: 100%;" :src="item.img" alt=""> </el-col> <el-col :span="12" style="margin-bottom: 10px"> <img style="width: 100%;" :src="item.img" alt=""> </el-col> </el-row> </div> <div style="margin: 10px 0; display: flex; line-height: 25px"> <div style="width: 50%"> <el-tag># 冬至到了</el-tag> <el-tag type="danger" style="margin-left: 10px"> <el-icon style="top: 1px"> <Compass/> </el-icon> 米粉杂谈 </el-tag> </div> <div style="width: 50%; text-align: right; color: #999; font-size: 14px;"> <el-icon size="20" style="top: 5px"> <View/> </el-icon> 20 <el-icon size="20" style="margin-left: 10px; top: 5px"> <Pointer/> </el-icon> 10 <el-icon size="20" style="margin-left: 10px; top: 5px"> <ChatLineRound/> </el-icon> 30 </div> </div> </div> </div> <!-- 咨询--> <div style="width: 40%; "> <div style=" padding: 10px; margin-left: 10px; border: 1px solid #ccc; border-radius: 10px; margin-bottom: 10px"> <div style="font-size: 18px; padding: 10px; color: salmon"><b>交友资讯</b></div> <div style="font-size: 14px; margin: 10px"><span style="color: goldenrod">1. </span> <span>经常不在家,如何让亲人听到你的声音?</span></div> <div style="font-size: 14px; margin: 10px"><span style="color: goldenrod">1. </span> <span>经常不在家,如何让亲人听到你的声音?</span></div> <div style="font-size: 14px; margin: 10px"><span style="color: goldenrod">1. </span> <span>经常不在家,如何让亲人听到你的声音?</span></div> <div style="font-size: 14px; margin: 10px"><span style="color: goldenrod">1. </span> <span>经常不在家,如何让亲人听到你的声音?</span></div> <div style="font-size: 14px; margin: 10px"><span style="color: goldenrod">1. </span> <span>经常不在家,如何让亲人听到你的声音?</span></div> </div> <div style=" padding: 10px; margin-left: 10px; border: 1px solid #ccc; border-radius: 10px"> <div style="font-size: 18px; padding: 10px; color: #8ec5fc"><b>推荐圈子</b></div> <el-row :gutter="10" style="margin: 10px 0"> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> <el-col :span="12"> <el-card style="margin-bottom: 10px; cursor: pointer"> <div style="padding: 5px; text-align: center">米粉圈子</div> </el-card> </el-col> </el-row> </div> </div> </div> </div> </template> <style scoped> /* 只显示2行文本,多余的用省略号代替 */ .content { margin: 10px 0; line-height: 25px; text-align: justify; word-break: break-all; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } </style>
问题一:不用登陆也可以看首页
解决:
1
2
3
// 直接进入首页,缓存里没用任何用户信息,会报错
const avatar = ref('')
const user = store.getUser // store.getUser 默认是 {},if({})是true
// 有值再赋值
if (user.avatar) {
avatar.value = user.avatar
}
问题二:
解决:
// role.vue const handleEdit = (raw) => { dialogFormVisible.value = true // 未来元素渲染 nextTick(() => { ruleFormRef.value.resetFields() state.form = JSON.parse(JSON.stringify(raw)) // 初始化,默认不选择任何节点 permissionTreeRef.value.setCheckedKeys([]) raw.permissionIds.forEach(v => { // 给权限树设置选中的节点 permissionTreeRef.value.setChecked(v, true, false) }) }) }
问题三:没登录 系统不知道是用户还是管理员 没法鉴权 。 需要把权限开放给用户
解决:
问题四:保存有数据,数据库有数据,但是前台未显示数据
解决:表单deleted字段未设置默认值
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。