vue+springboot实现聊天功能

前言

在我的项目中,突然有种想法,想实现聊天功能,历经一段时间终于做出来了;那么接下来会讲解如何实现,这篇文章只会实现最基础的逻辑,实时获取对方聊天记录,话不多说,我们就开始吧

实际项目演示

我的项目中,分为了两部分,一部分为用户区,一部分为聊天区

🍁🍁🍁🍁🍁🍁🍁

在这里插入图片描述

🌰🌰🌰🌰🌰🌰🌰

用户区,通过回车获取指定用户名称的用户
ps:这里可根据自己的需求直接获取所有人员或者在线人员的数据

在这里插入图片描述
当点击对应用户,可获取对应用户和自己的聊天记录

己方视角:

在这里插入图片描述
对方视角
在这里插入图片描述

建表

我的项目中连接的是mysql数据库,对应建表有如下:

用户信息表

CREATE TABLE `user` (`user_id` varchar(20) NOT NULL COMMENT '用户id',`avatar` longtext COMMENT '头像',`user_name` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(50) NOT NULL COMMENT '密码',`salt` varchar(128) DEFAULT NULL COMMENT '加密盐值',`email` varchar(50) DEFAULT NULL COMMENT '邮箱',`phone` varchar(50) DEFAULT NULL COMMENT '联系方式',`sex` varchar(50) DEFAULT NULL COMMENT '性别',`age` int(3) DEFAULT NULL COMMENT '年龄',`status` int(1) NOT NULL COMMENT '用户状态:1有效; 0删除',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

发送信息表

CREATE TABLE `message` (`handle` varchar(40) NOT NULL COMMENT '主键',`send_user` varchar(20) DEFAULT NULL COMMENT '发送人',`receive_user` varchar(20) DEFAULT NULL COMMENT '接收人',`content` varchar(500) DEFAULT NULL COMMENT '留言内容',`is_read` tinyint(1) DEFAULT '0' COMMENT '是否已读',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '留言时间',PRIMARY KEY (`handle`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='留言板';

其中有一点,为了能够发送emoji表情,如😂😃😍😘等等一系列数据,需要将我们的数据库设置为utf8mb4类型,以及对应接收发送信息的表字段设置为utf8mb4类型

设置数据库

 ALTER DATABASE 数据库名称 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

设置指定表字段

ALTER TABLE 表名 MODIFY 字段名称 VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

以上,我们就满足了我们最最基础的聊天功能了

🍉🍊🍋🍉🍊🍋🍉🍊🍋

前端

依赖安装

在开始之前,请安装相关依赖

npm i axios@1.5.0
npm i element-ui -S

mian.js

主入口逻辑

// 如下省略router
import Vue from 'vue'
import App from './App'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = falseVue.use(ElementUI)
new Vue({el: '#app',components: { App },template: '<App/>'
})

request.js

axios封装类

import axios from 'axios'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'// 创建axios实例
const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: process.env.VUE_APP_BASE_API,// 请求超时时间 withCredentials: true,timeout: 30000
})// 请求统一拦截处理
service.interceptors.request.use(config => {return config
},
error => {// 请求失败console.log(error) // for debugreturn Promise.reject(error)
}
)// 响应拦截器
service.interceptors.response.use(res => {console.log('res.data', res.data)
}, error => {return Promise.reject(error)
}
)export default service

axios调用后端js

封装需要的方法

user.js

// 位置换成自己项目位置
import request from '@/utils/request'// 根据用户名查用户
export function searchUserByUserName (userName) {return request({url: '/user/searchUserByUserName',method: 'get',params: {userName}})
}

message.js

import request from '@/utils/request'// 发送信息
export function sendMessage (data) {return request({url: '/message/sendMessage',method: 'post',data: data})
}// 根据发送用户和接收用户获取聊天记录
export function findMessageBySendUserAndReceiveUser (sendUserId, receiveUserId) {return request({url: '/message/findMessageBySendUserAndReceiveUser',method: 'get',params: {sendUserId,receiveUserId}})
}

聊天界面逻辑

界面

<template><div class="chat-container"><!-- Left side: User list --><div class="left-side"><!-- Search input (moved outside) --><div class="search-wrapper"><el-input v-model="searchUserName" placeholder="回车搜索用户" class="search-input" @keydown.enter.native="searchUsers"></el-input></div><!-- User list (with scroll) --><el-scrollbar class="user-list-scroll"><el-menu><el-menu-item v-for="user in filteredUsers" :key="user.id" @click="chooseUser(user)" class="user-item"><el-avatar :src="user.avatar" size="medium"></el-avatar><span>{{ user.userName }}</span><span slot="title">{{ user.lastMessage }}</span></el-menu-item></el-menu></el-scrollbar></div><!-- Right side: Chat box --><div class="right-side"><!-- Chat header --><div class="chat-header"><span v-if="currentUser">{{ currentUser.userName }}</span></div><!-- Chat messages --><el-scrollbar class="chat-messages"><div class="messageBox" v-for="message in messages" :key="message.handle" :class="{ ownMessage: message.sendUser === loginUserId, otherMessage: message.sendUser !== loginUserId }"><div><img :src="message.sendUser === loginUserId ? loginUser.avatar : currentUser.avatar" alt=""></div><div class="messageContent">{{ message.content }}</div><!-- 这里逻辑我是为了时间格式化,请按照自己项目实际修改 --><div class="messageTime">{{ message.createTime.replace('T', ' ') }}</div></div></el-scrollbar><div class="chat-input"><el-input v-model="newMessage.content" placeholder="请输入聊天内容" autosize class="message-input"></el-input><el-button type="primary" @click.native="sendMessage" class="send-button">发送</el-button></div></div></div>
</template>

js逻辑

<script>
import {searchUserByUserName} from '../pc/../../api/user'
import {findMessageBySendUserAndReceiveUser, sendMessage} from '../pc/../../api/message'export default {data () {return {intervalId: null, // 定时调用,实现实时获取聊天记录users: [],filteredUsers: [],currentUser: null, // 当前聊天人员用户信息loginUser: null, // 登录人员用户信息messages: [],newMessage: {handle: '',sendUser: '',receiveUser: '',content: '',is_read: '0',createTime: ''},loginUserId: '', // 登录人员userIdsearchUserName: '',}},methods: {async fetchMessages (userId) {// 传当前聊天人员的userIdif (!userId) {return}if (this.loginUserId== null) {this.$message.error('登录用户编号获取失败,请重新登录!')return}findMessageBySendUserAndReceiveUser(userId, localCommon.userInfo.userId).then(res => {console.log('消息记录', res)if (res.header.code !== 0) {this.$message.error(res.header.message)return}// 赋值最终的聊天信息,根据自己项目调整赋值this.messages = res.value})},sendMessage () {if (!this.newMessage.content.trim()) {this.$message.warning('请输入聊天内容')return}if (this.loginUserId== null) {this.$message.error('登录用户编号获取失败,请重新登录!')return}this.newMessage.sendUser = this.loginUserIdthis.newMessage.receiveUser = this.currentUser.userIdconsole.log('需要发送信息', this.newMessage)sendMessage(this.newMessage).then(res => {console.log('发送信息:', res)if (res.header.code !== 0) {this.$message.error(res.header.message)return}// 发送完之后获取聊天记录更新this.chooseUser(this.currentUser)})},// 设置不同用户的头像checkAvatar (message) {if (message.sendUser === this.loginUserId) {console.log('发送人头像:', this.currentUser)return this.currentUser.avatar} else {console.log('登录人头像:', this.loginUser)return this.loginUser.avatar}},chooseUser (user) {this.currentUser = userthis.fetchMessages(user.userId)},searchUsers () {if (!this.searchUserName) {this.$message.error('用户名不能为空!')return}searchUserByUserName(this.searchUserName).then(res => {console.log('搜索用户:', res)if (res.header.code !== 0) {this.$message.error(res.header.message)return}this.filteredUsers = res.value})},},mounted () {// 定时自动this.intervalId = setInterval(() => {this.fetchMessages(this.currentUser.userId)}, 3000)},destroyed () {// 在组件销毁前清除定时器,防止内存泄漏clearInterval(this.intervalId)},created () {// 通过登录人的userId获取用户信息searchUserByUserName(this.loginUserId).then(res => {if (res.header.code === 0) {if (res.value) {this.loginUser = res.value[0]}}})}
}
</script>

以上的登录人userId,请根据自己实际项目进行赋值
✨✨✨✨✨✨

样式

<style scoped>
.chat-container {display: flex;height: 100%;background: linear-gradient(to bottom right, #FFFFFF, #ECEFF1);
}.left-side {position: relative; /* Position relative for absolute positioning */flex: 1;padding: 20px;border-right: 1px solid #eaeaea;border-radius: 10px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}.search-input {position: absolute;top: 20px;left: 20px;width: calc(100% - 40px);max-width: 300px;
}.user-list-scroll {top: 40px;height: calc(100% - 40px);overflow-y: auto;
}.right-side {flex: 3;display: flex;flex-direction: column;
}.chat-header {padding: 20px;border-bottom: 1px solid #eaeaea;font-size: 1.2em;color: #37474F;
}.chat-messages {flex: 1;overflow-y: auto;padding: 20px;
}
.chat-input {padding: 20px;display: flex;align-items: center;
}.message-input {flex: 1;margin-right: 10px;
}.send-button {flex-shrink: 0;
}.user-item {display: flex;align-items: center;padding: 10px;
}.user-item:hover {background-color: #E0E0E0;cursor: pointer;transition: background-color 0.3s ease;
}.user-item .el-avatar {margin-right: 10px;
}.user-item .el-list-item-content {flex: 1;
}
.editor {border-radius: 5px;box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.search-input {position: relative;z-index: 999;
}.messageBox {display: flex;align-items: flex-start; /* 将头像和文本第一行对齐 */margin-bottom: 10px;
}.messageBox img {width: 40px; /* 调整头像大小 */height: 40px;border-radius: 50%;margin-right: 10px;margin-left: 10px;
}.messageContent {max-width: 70%; /* 调整发送信息宽度 */padding: 10px;border-radius: 8px;background-color: #f0f0f0;text-align: left; /* 文本左对齐 */word-wrap: break-word; /* 当文本过长时自动换行 */
}.messageTime {font-size: 12px;color: #999;margin-left: 10px;margin-top: 5px; /* 将发送时间与文本分隔开 */
}.ownMessage {flex-direction: row-reverse;align-items: flex-end; /* 将发送时间放置在最下方的贴右位置 */
}.otherMessage {flex-direction: row;align-items: flex-end; /* 将发送时间放置在最下方的贴左位置 */
}
</style>

后端

application.yml

对应的数据库连接逻辑需要调整为如下:

spring:datasource:mysql:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/数据库名?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&useCompression=false&useSSL=falseusername: 账号password: 密码druid:connection-init-sqls: set names utf8mb4 # 初始化为utf8mb4

查询用户接口

实际上这个接口对于你们需要用的没有参考价值,需要根据自己的实际项目调整的,但是这边也给出代码示例

controller

  @GetMapping("/searchUserByUserName")public Response searchUserByUserId(@RequestParam("userName") String userName) {try (Connection conn = primeDB.create()) {return new Response(0, userService.searchUserByUserId(conn, userName), "检索成功!");} catch (Exception e) {return new Response(1, e.getMessage());}}

service

  public List<User> searchUserByUserId(Connection conn, String userName) throws Exception {try {AssertUtils.isError(StringUtils.isEmpty(userName), "用户编号不能为空!");// 我的项目的数据库连接代码UserDao userDao = new UserDao(conn);List<User> userList = userDao.selectByUserName(userName);// 只查有效状态账号的数据List<User> filteredUserList = userList.stream().filter(o -> o.getStatus() == 1).collect(Collectors.toList());return filteredUserList;} catch (Exception e) {throw new Exception(e.getMessage());}}

信息发送逻辑

controller

  @PostMapping("/sendMessage")public Response sendMessage(@RequestBody Message message) {try (Connection conn = primeDB.create()) {messageService.sendMessage(conn, message);return new Response(0, "发送成功!");} catch (Exception e) {return new Response(1, e.getMessage());}}@GetMapping("/findMessageBySendUserAndReceiveUser")public Response<List<Message>> findMessageBySendUserAndReceiveUser(@RequestParam("sendUserId") String sendUserId,@RequestParam("receiveUserId") String receiveUserId) {try (Connection conn = primeDB.create()) {return new Response<>(0,messageService.findMessageBySendUserAndReceiveUser(conn, sendUserId, receiveUserId),"查找成功!");} catch (Exception e) {return new Response<>(1, e.getMessage());}}

service

// 发送信息逻辑,请根据自己实际项目调整public void sendMessage(Connection conn, Message message) throws Exception {try {AssertUtils.isError(StringUtils.isEmpty(message.getSendUser()), "发送用户不能为空!");AssertUtils.isError(StringUtils.isEmpty(message.getReceiveUser()), "接收用户不能为空!");AssertUtils.isError(StringUtils.isEmpty(message.getContent()), "发送信息不能为空!");UserDao userDao = new UserDao(conn);MessageDao messageDao = new MessageDao(conn);User sendUser = userDao.selectbyUserId(message.getSendUser());AssertUtils.isError(null == sendUser, "发送用户不存在,发送信息失败!");AssertUtils.isError(sendUser.getStatus() != 1,"发送用户:" + message.getSendUser() + "状态已冻结,无法发送信息!");User receiveUser = userDao.selectbyUserId(message.getReceiveUser());AssertUtils.isError(null == receiveUser, "接收用户不存在,发送信息失败!");AssertUtils.isError(receiveUser.getStatus() != 1,"接收用户:" + message.getReceiveUser() + "状态已冻结,无法接收信息!");message.setHandle(UUID.randomUUID().toString());message.setIsRead("0");message.setCreateTime(LocalDateTime.now());messageDao.insert(message);} catch (Exception e) {throw new Exception(e.getMessage());}}// 获取两个用户之间的聊天记录public List<Message> findMessageBySendUserAndReceiveUser(Connection conn, String sendUserId,String receiveUserId) throws Exception {try {AssertUtils.isError(StringUtils.isEmpty(sendUserId), "发送用户为空!");AssertUtils.isError(StringUtils.isEmpty(receiveUserId), "接收用户为空!");UserDao userDao = new UserDao(conn);User sendUser = userDao.selectbyUserId(sendUserId);AssertUtils.isError(null == sendUser, "发送用户不存在,发送信息失败!");User receiveUser = userDao.selectbyUserId(receiveUserId);AssertUtils.isError(null == receiveUser, "接收用户不存在,发送信息失败!");MessageDao messageDao = new MessageDao(conn);// 获取对方发送的信息,限制指定条数,防止聊天数量太多查询很慢List<Message> receiveMessageList = messageDao.selectBySendUserAndReceiveUserLimitLength(sendUserId,receiveUserId, 100);// 获取发送给对方的信息List<Message> sendMessageList = messageDao.selectBySendUserAndReceiveUserLimitLength(receiveUserId,sendUserId, 100);List<Message> allMessageList = new ArrayList<>();allMessageList.addAll(receiveMessageList);allMessageList.addAll(sendMessageList);// 将两个用户互相发送给对方的信息放到集合按照时间排序,即可实现聊天交互逻辑List<Message> sortedMessageList = allMessageList.stream().sorted(Comparator.comparing(Message::getCreateTime)).collect(Collectors.toList());return sortedMessageList;} catch (Exception e) {throw new Exception(e.getMessage());}}

以上我们的基础的聊天功能就实现好了,可喜可贺🎉🎉🎉

最后

以上,如一开头所说,只实现了最基础的聊天功能

其实还能够做优化,如

  1. 用户区展示最新聊天记录
  2. 设置聊天记录已读未读
  3. 聊天内容是否可发图片 or 其他内容

    等等一系列调整,这边就不多赘述,给出基础的聊天逻辑供参考

🎈🎈🎈🎈🎈🎈

结语

以上为vue+springboot实现聊天功能,后面计划开一篇文章讲解如何通过websocket来进行实时通讯,来实现聊天功能

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

吹爆,一款实用的个人IT工具箱

作为一名开发人员&#xff0c;我们在日常工作和学习中常常需要使用一系列小工具&#xff0c;如JSON格式化、JSON转表格、当前时间戳、XML格式化、SQL格式化、密码生成以及UUID生成等。通常情况下&#xff0c;我们会在网上搜索各种在线工具来满足这些需求。 然而&#xff0c;这…

【简单介绍下单片机】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

大学生简历大赛演讲稿(6篇)

大学生简历大赛演讲稿&#xff08;6篇&#xff09; 以下是六篇大学生简历大赛演讲稿的范文&#xff0c;供您参考&#xff1a; 范文一&#xff1a;展现真我&#xff0c;点亮未来 尊敬的评委、亲爱的同学们&#xff1a; 大家好&#xff01; 今天&#xff0c;我站在这里&#xf…

【C++】:C++关键字,命名空间,输入输出,缺省参数

目录 一&#xff0c;C关键字(C98)二&#xff0c;命名空间2.1 命名冲突2.2 关键字namespace2.2.1 命名空间中可以定义变量/函数/类型2.2.2 命名空间可以嵌套2.2.3 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。 2.3 命名空间的使用2.3.1 指定…

剑指offer之牛客与力扣——前者分类题单中的题目在后者的链接

搜索 [4.12完成] JZ1 LCR 172. 统计目标成绩的出现次数 JZ3 153. 寻找旋转排序数组中的最小值 JZ4 LCR 014. 字符串的排列 JZ5 LCR 163. 找到第 k 位数字 400 动态规划 [4.15完成] JZ2 LCR 161. 连续天数的最高销售额 53 JZ3 LCR 127. 跳跃训练 70 JZ4 LCR 126. 斐波那契…

gemini国内怎么用

gemini国内怎么用 Google Gemini 作为一个尚处于研发阶段的大型语言模型&#xff0c;其具体功能和性能尚未公开&#xff0c;因此无法对其好用程度做出明确评价。 然而&#xff0c;基于 Google 在人工智能领域的领先地位和技术实力&#xff0c;我们可以对其潜力进行一些推测&a…

大型网站系统架构演化实例_4.数据库读写分离

1.数据库读写分离 网站在使用缓存后&#xff0c;使对大部分数据读操作访问都可以不通过数据库就能完成&#xff0c;但是仍有一部分操作&#xff08;缓存访问不命中、缓存过期&#xff09;和全部的写操作都需要访问数据库&#xff0c;在网站的用户达到一定规模后&#x…

通过实例学C#之ArrayList

介绍 ArrayList对象可以容纳若干个具有相同类型的对象&#xff0c;那有人说&#xff0c;这和数组有什么区别呢。其区别大概可以分为以下几点&#xff1a; 1.数组效率较高&#xff0c;但其容量固定&#xff0c;而且没办法动态改变。 2.ArrayList容量可以动态增长&#xff0c;但…

ros1中python3包调用自定义.py文件

ros中python包相互import不成功问题 问题解决办法 问题 在ros工程中&#xff0c;运行python文件难以直接import自己写的py文件&#xff0c;相互之间无法import&#xff0c;但是在python3虚拟环境python *.py文件就可以正常运行&#xff01; 注意这里还有个问题&#xff0c;我…

❤️‍FlyFlow工作流周更来咯~~

FlyFlow 借鉴了钉钉与飞书的界面设计理念&#xff0c;致力于打造一款用户友好、快速上手的工作流程工具。相较于传统的基于 BPMN.js 的工作流引擎&#xff0c;我们提供的解决方案显著简化了操作逻辑&#xff0c;使得用户能够在极短的时间内构建定制化的业务流程&#xff0c;即便…

记录汇川:五个ST案例

起保停&#xff1a; 简单数学教学&#xff1a; 数据查找&#xff1a; 按钮检测&#xff1a; 数据堆栈&#xff1a;

wiringpi库的应用 -- sg90 定时器 oled

sg 90舵机: 接线: VCC -- 红 GND -- 地 信号线 -- 黄 -- pwm 定时器: 先玩定时器: sg90 需要的pwm波需要定时器输出&#xff0c;so我们得先来玩一下定时器 分析&#xff1a;实现定时器&#xff0c;通过itimerval结构体以及函数setitimer产生的信号&#xff0c;系统…

快手本地生活服务商系统怎么操作?

当下&#xff0c;抖音和快手两大短视频巨头都已开始布局本地生活服务&#xff0c;想要在这一板块争得一席之地。而这也很多普通人看到了机遇&#xff0c;选择成为抖音和快手的本地生活服务商&#xff0c;通过将商家引进平台&#xff0c;并向其提供代运营服务&#xff0c;而成功…

深入探讨虚拟现实中的新型安全威胁:“盗梦攻击”及其防御策略

随着虚拟现实&#xff08;VR&#xff09;技术的飞速发展&#xff0c;用户体验达到了前所未有的沉浸水平&#xff0c;但也暴露在一系列新的安全威胁之下。本文着重介绍了近期出现的一种高度隐秘且影响深远的攻击手段——“盗梦攻击”。这一概念由芝加哥大学的研究人员提出&#…

前端打包webpack vite

起步 | webpack 中文文档 | webpack中文文档 | webpack中文网 npm run build 1webpack: mkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli --save-dev vite : 快速上手 | Vue.js

【Entity Framework】闲话EF中批量配置

【Entity Framework】闲话EF中批量配置 文章目录 【Entity Framework】闲话EF中批量配置一、概述二、OnModelCreating中的批量配置元数据API的缺点 三、预先约定配置忽略类型默认类型映射预先约定配置的限制约定添加新约定替换现有约定约定实现注意事项 四、何时使用每种方法进…

游戏登录界面制作

登录界面制作 1.导入模块和初始化窗口 import subprocessimport tkinter as tkimport picklefrom tkinter import messageboxwindow tk.Tk()window.title(Welcome)window.geometry(450x300) 导入必要的模块&#xff0c;并初始化了主窗口window&#xff0c;设置了窗口的标题和…

修改taro-ui-vue3的tabs组件源码增加数字标签

需求&#xff1a;taro-ui-vue3的tabs组件上增加数字标记 步骤一&#xff1a;node_modules文件夹下找到taro-ui-vue3/lib/tabs/index.js 把173行的这一段替换成下面这段&#xff0c;然后写上样式 default: () > item.number ? [h(View, {class: at-tabs__item_in}, {defau…

Unity导出package

C#代码导出后为一个dll&#xff0c;原有的不同平台的库不变。 以下操作均在build PC 平台下操作。 1.在要导出的文件夹下建assembly definition (Any platform) 2.将项目文件夹下的\Library\ScriptAssemblies中的相应assembly definition的dll复制到要导出的文件夹下 3.在uni…

基于Java+SpringBoot+Vue前后端分离仓库管理系统

基于JavaSpringBootVue前后端分离仓库管理系统 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制系统 &#…