前言
在我的项目中,突然有种想法,想实现聊天功能,历经一段时间终于做出来了;那么接下来会讲解如何实现,这篇文章只会实现最基础的逻辑,实时获取对方聊天记录,话不多说,我们就开始吧
实际项目演示
我的项目中,分为了两部分,一部分为用户区,一部分为聊天区
🍁🍁🍁🍁🍁🍁🍁
🌰🌰🌰🌰🌰🌰🌰
用户区,通过回车获取指定用户名称的用户
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 = false
Vue.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 debug
return 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-list>
<el-list-item v-for="user in filteredUsers" :key="user.id" @click="chooseUser(user)" class="user-item">
<el-avatar :src="user.avatar" size="medium"></el-avatar>
<el-list-item-content>
<el-list-item-title>{{ user.userName }}</el-list-item-title>
<el-list-item-subtitle>{{ user.lastMessage }}</el-list-item-subtitle>
</el-list-item-content>
</el-list-item>
</el-list>
</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: '', // 登录人员userId
searchUserName: '',
}
},
methods: {
async fetchMessages (userId) {
// 传当前聊天人员的userId
if (!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.loginUserId
this.newMessage.receiveUser = this.currentUser.userId
console.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 = user
this.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.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/数据库名?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&useCompression=false&useSSL=false
username: 账号
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());
}
}
以上我们的基础的聊天功能就实现好了,可喜可贺🎉🎉🎉
最后
以上,如一开头所说,只实现了最基础的聊天功能
其实还能够做优化,如
- 用户区展示最新聊天记录
- 设置聊天记录已读未读
- 聊天内容是否可发图片 or 其他内容
…
等等一系列调整,这边就不多赘述,给出基础的聊天逻辑供参考
🎈🎈🎈🎈🎈🎈
结语
以上为vue+springboot实现聊天功能,后面计划开一篇文章讲解如何通过websocket来进行实时通讯,来实现聊天功能