简要
该文章只是作者从自己开发的代码中截取的一部分,只是做一个参考;实际上需要自己在该代码基础上进行调整和优化,有疑问可以在评论区进行提问
一、数据库ER设计
聊天功能主要涉及到两张表,message和user表,message用来存信息,user表用来关联用户信息,主要是拿来取用户昵称以及头像
message表创建:
CREATE TABLE `chat_message` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增主键id',
`send_user_id` varchar(20) NOT NULL COMMENT '发送用户id',
`accept_user_id` varchar(20) NOT NULL COMMENT '接手用户id',
`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息类型(图片:img,视频:video,文本:text)',
`content` text COMMENT '发送内容',
`readed` int NOT NULL DEFAULT '0' COMMENT '是否阅读',
`delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
`send_time` datetime NOT NULL COMMENT '发送时间',
PRIMARY KEY (`id`),
KEY `user_id` (`send_user_id`,`accept_user_id`) USING BTREE COMMENT 'userId索引'
) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
user表创建:
CREATE TABLE `wx_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增id',
`user_id` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户ID',
`nickName` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '昵称',
`headImg` varchar(150) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '头像链接',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '电话',
`openid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'openID',
`unionid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'unionID',
`status` int NOT NULL DEFAULT '0' COMMENT '状态 0:使用中 1:冻结中 2:长时间未使用',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`modefied_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `user_id` (`user_id`),
UNIQUE KEY `openID` (`openid`) USING BTREE,
UNIQUE KEY `unionID` (`unionid`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
KEY `id` (`id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
二、后端接口设计
entity:
Message.java
package com.example.wxapi.entity.MessageEntity;
import com.example.wxapi.entity.UserEntity.WxUser;
import lombok.Data;
@Data
public class ChatMessage {
private Integer id;
private String sendUserId;
private String acceptUserId;
private String type;
private String content;
private Integer soundTIme;
private String sendTime;
private Integer readedNum;
private WxUser wxUser;
}
WxUser.java
package com.example.wxapi.entity.UserEntity;
import lombok.Data;
@Data
public class WxUser {
private Integer id;
private String userId;
private String nickName;
private String headImg;
private String phone;
private String openid;
private String unionid;
private Integer status;
private String createdTime;
private String modefiedTime;
}
mapper层:
MessageMapper.xml文件
SELECT
bcm.id,
bcm.send_user_id sendUserId,
bcm.accept_user_id acceptUserId,
wx.nickName,
wx.headImg,
bcm.content,
bcm.sound_time soundTime,
bcm.type,
bcm.send_time sendTime
FROM
base.chat_message bcm left JOIN base.wx_user wx on bcm.send_user_id = wx.user_id
WHERE
( bcm.send_user_id = #{sendUserId} AND bcm.accept_user_id = #{acceptUserId} AND bcm.`delete` = 0 )
OR ( bcm.send_user_id = #{acceptUserId} AND bcm.accept_user_id = #{sendUserId} AND bcm.`delete` = 0 )
ORDER BY
bcm.send_time DESC
SELECT
tmp.userId acceptUserId,
wu.nickName,
wu.headImg,
cm.content,
cm.type,
cm.send_time sendTime,
( SELECT count( * ) FROM base.chat_message WHERE send_user_id = tmp.userId AND readed = 0 ) readedNum
FROM
(
SELECT
max( allMsg.msgId ) msgId,
allMsg.userId userId
FROM
(
( SELECT max( id ) msgId, send_user_id userId FROM base.chat_message WHERE accept_user_id = #{userId} GROUP BY send_user_id ) UNION ALL
( SELECT max( id ) msgId, accept_user_id userId FROM base.chat_message WHERE send_user_id = #{userId} GROUP BY accept_user_id )
) allMsg
GROUP BY
allMsg.userId
) tmp
INNER JOIN base.chat_message cm ON cm.id = tmp.msgId
INNER JOIN base.wx_user wu ON wu.user_id = tmp.userId
ORDER BY cm.send_time DESC
SELECT
COUNT( * ) noReadNum
FROM
base.chat_message
WHERE
accept_user_id = #{userId}
AND readed = 0
UPDATE base.chat_message
SET readed = 1
WHERE
accept_user_id = #{acceptUserId}
AND send_user_id = #{sendUserId}
UPDATE base.chat_message SET `delete` = 1 WHERE id = #{msgId}
INSERT INTO base.chat_message
send_user_id,
accept_user_id,
content,
sound_time,
`type`,
send_time
#{sendUserId},
#{acceptUserId},
#{content},
#{soundTime},
#{type},
#{sendTime}
dao层:
MessageMapper.java
package com.example.wxapi.dao.personIndexMapper;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MessageMapper {
List
List
Boolean sendMsg(String sendUserId,String acceptUserId,String content,String type,Integer soundTime,String sendTime);
Boolean readedMsg(String sendUserId,String acceptUserId);
int getAllNoReadMsgNum(String userId);
Boolean delMsg(int msgId);
}
service层:
MessageService.java
package com.example.wxapi.service.personService;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.entity.MessageEntity.SystemMessage;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
public interface MessageService {
Map
Map
Map
Map
Boolean readedMsg(String sendUserId,String acceptUserId);
int getAllNoReadMsgNum(String userId);
}
注入类 MessageServiceImpl.java
package com.example.wxapi.service.implement.personServiceImpl;
import com.example.wxapi.component.UploadFile;
import com.example.wxapi.dao.personIndexMapper.MessageMapper;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.service.personService.MessageService;
import com.example.wxapi.tools.Time;
import com.example.wxapi.webSocket.WebSocketServer;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class MessageServiceImpl implements MessageService {
@Resource
private MessageMapper messageMapper;
@Resource
private WebSocketServer webSocketServer;
/**
* 获取聊天好友信息
* @param userId
* @param pageNum
* @param pageSize
* @return
*/
@Override
public Map
Map
PageHelper.startPage(pageNum,pageSize);
PageInfo
resData.put("pagesNum",info.getPages());
resData.put("totalNum",info.getTotal());
resData.put("size",info.getSize());
resData.put("data", info.getList());
return resData;
}
/**
* 获取聊天信息
* @param sendUserId
* @param acceptUserId
* @param pageNum
* @param pageSize
* @return
*/
@Override
public Map
Map
PageHelper.startPage(pageNum,pageSize);
PageInfo
resData.put("pagesNum",info.getPages());
resData.put("totalNum",info.getTotal());
resData.put("size",info.getSize());
resData.put("data", info.getList());
return resData;
}
/**
* 发送消息
* @param chatMessage
* @return
*/
@Override
public Map
Map
if(messageMapper.sendMsg(chatMessage.getSendUserId(), chatMessage.getAcceptUserId(), chatMessage.getContent(), chatMessage.getType(), chatMessage.getSoundTIme(),Time.getTime("yyyy-MM-dd HH:mm:ss"))) {
try {
List
webSocketServer.send(chatMessage.getSendUserId(), chatMessage.getAcceptUserId(), newMsg.get(0));
repData.put("status",true);
repData.put("returnMsg",newMsg.get(0));
} catch (IOException e) {
log.info("发送失败!");
}
}
return repData;
}
/**
* 发送聊天文件
* @param sendUserId
* @param acceptUserId
* @param type
* @param file
* @return
*/
@Override
public Map
Map
Map
if ((Boolean) res.get("status")) {
if (messageMapper.sendMsg(sendUserId,acceptUserId,(String) res.get("fileUrl"),type,time,Time.getTime("yyyy-MM-dd HH:mm:ss"))) {
List
try {
webSocketServer.send(sendUserId, acceptUserId, newMsg.get(0));
}catch (IOException e) {
log.info("发送失败!");
}
repData.put("status",true);
repData.put("returnMsg",newMsg.get(0));
}
}else {
repData.put("status",false);
}
return repData;
}
/**iu i
* 已读消息
* @param sendUserId
* @param acceptUserId
* @return
*/
@Override
public Boolean readedMsg(String sendUserId, String acceptUserId) {
return messageMapper.readedMsg(sendUserId,acceptUserId);
}
/**
* 所有未读数
* @param userId
* @return
*/
@Override
public int getAllNoReadMsgNum(String userId) {
return messageMapper.getAllNoReadMsgNum(userId);
}
}
controller层:
MsgApi.java
package com.example.wxapi.controller.personIndexApi;
import com.example.wxapi.dao.personIndexMapper.MessageMapper;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.global.JsonResult;
import com.example.wxapi.service.implement.personServiceImpl.MessageServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
import java.util.Objects;
/**
* JsonResult 为自定义json序列化方法,用自己的方法即可
*
**/
@RestController
@RequestMapping("/msg")
public class MsgApi {
@Resource
private MessageServiceImpl messageService;
@Resource
private MessageMapper messageMapper;
/**
* 获取好友列表API
* @param userId
* @param pageNum
* @param pageSize
* @return
*/
@PostMapping("/getFriendMsgList")
public JsonResult getFriendMsgList(@RequestParam(value = "userId") String userId,
@RequestParam(value = "pageNum") int pageNum,
@RequestParam(value = "pageSize") int pageSize) {
return JsonResult.success(messageService.getFriendMsgList(userId,pageNum,pageSize));
}
/**
* 获取聊天信息API
* @param sendUserId
* @param acceptUserId
* @param pageNum
* @param pageSize
* @return
*/
@PostMapping("/getChatMessage")
public JsonResult getChatMessage(@RequestParam(value = "sendUserId") String sendUserId,
@RequestParam(value = "acceptUserId") String acceptUserId,
@RequestParam(value = "pageNum") int pageNum,
@RequestParam(value = "pageSize") int pageSize) {
return JsonResult.success(messageService.getChatMessage(sendUserId,acceptUserId,pageNum,pageSize));
}
/**
* 发送消息API
* @param chatMessage
* @return
*/
@PostMapping("sendMsg")
public JsonResult sendMsg(@RequestBody ChatMessage chatMessage) {
Map
if ((Boolean) repData.get("status"))
return JsonResult.success(repData.get("returnMsg"));
return JsonResult.fail();
}
/**
* 发送聊天文件API
* @param sendUserId
* @param acceptUserId
* @param type
* @param time
* @param file
* @return
*/
@PostMapping("/sendFileMsg")
public JsonResult sendFileMsg(@RequestParam(value = "sendUserId") String sendUserId,
@RequestParam(value = "acceptUserId") String acceptUserId,
@RequestParam(value = "type") String type,
@RequestParam(value = "time", required = false) Integer time,
@RequestParam(value = "file")MultipartFile file
) {
Map
if ((Boolean) resData.get("status"))
return JsonResult.success(resData.get("returnMsg"));
else
return JsonResult.fail("发送失败!");
}
/**
* 已读消息API
* @param sendUserId
* @param acceptUserId
* @return
*/
@GetMapping("/readedMsg")
public JsonResult readedMsg(@RequestParam("sendUserId") String sendUserId,
@RequestParam("acceptUserId") String acceptUserId) {
if (messageService.readedMsg(sendUserId,acceptUserId))
return JsonResult.success();
return JsonResult.fail(200,"已读失败");
}
/**
* 获取所有消息未读数API
* @param userId
* @return
*/
@GetMapping("/getAllNoReadMsgNum")
public JsonResult getAllNoReadMsgNum(@RequestParam("userId") String userId) {
return JsonResult.success(messageService.getAllNoReadMsgNum(userId));
}
/**
* 删除消息
* @param msgId
* @return
*/
@DeleteMapping("/delMsg")
public JsonResult delMsg(int msgId) {
if (messageMapper.delMsg(msgId))
return JsonResult.success();
return JsonResult.fail();
}
}
最重要的东西来了,Websocket服务
WebSocketServer.java
package com.example.wxapi.webSocket;
import com.alibaba.fastjson2.JSON;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@ServerEndpoint("/websocket/{uid}")
@Component
public class WebSocketServer {
private static int onlineCount = 0;
private Session session;
private String uid;
private static final ConcurrentHashMap
/**
* 连接
* @param session
* @param uid
*/
@OnOpen
public void onOpen(Session session, @PathParam("uid") String uid) {
this.session = session;
this.uid = uid;
if(webSocketMap.containsKey(uid)) {
webSocketMap.remove(uid);
webSocketMap.put(uid,this);
}else {
webSocketMap.put(uid,this);
onlineCount++;
}
log.info("用户:{} 连接成功,当前在线人数:{}",uid,onlineCount);
}
/**
* 关闭连接
*/
@OnClose
public void onClose() {
if(webSocketMap.containsKey(uid))
onlineCount--;
webSocketMap.remove(uid);
log.info("用户:{} 已退出连接,当前在线人数:{}",uid,onlineCount);
}
/**
* 监听消息
* @param content
* @param session
*/
@OnMessage
public void OnMessage(String content,Session session) {
log.info("用户:{} 发送内容:{}",uid,content);
}
/**
* 服务推送消息
* @param content
* @throws IOException
*/
public void sendMessage(String content) throws IOException {
this.session.getBasicRemote().sendText(content);
}
/**
* 发送消息
* @param uid
* @param toUid
* @param content
* @throws IOException
*/
public void send(String uid,String toUid,Object content) throws IOException {
if(webSocketMap.containsKey(toUid)) {
Map
msgInfo.put("sender",uid);
msgInfo.put("acceptor",toUid);
msgInfo.put("msg",content);
webSocketMap.get(toUid).sendMessage(JSON.toJSONString(msgInfo));
log.info("用户:{} 向用户: {} 发送了信息:{}",uid,toUid,content);
}
else {
log.info("用户:{} 没在线",toUid);
}
}
}
上传文件到文件服务器的uploadFile方法
UploadFile.java
package com.example.wxapi.component;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@Component
@Slf4j
public class UploadFile {
//远程文件服务器地址
private static final String FILE_URL="http://xxxxxxxx"
public static Map
Map
//文件服务器url
String path = FILE_URL;
//为上传到服务器的文件取名,使用UUID防止文件名重复
String type= Objects.requireNonNull(File.getOriginalFilename()).substring(File.getOriginalFilename().lastIndexOf("."));
String fileNicKName= UUID.randomUUID() +type;
String fileName = File.getOriginalFilename();
String fileUrl = path + fileType + fileNicKName;
try{
//使用Jersey客户端上传文件
Client client = Client.create();
WebResource webResource = client.resource(path + fileType + URLEncoder.encode(fileNicKName, StandardCharsets.UTF_8));
webResource.put(File.getBytes());
map.put("status",true);
map.put("fileName",fileName);
map.put("fileUrl",fileUrl);
log.info("文件名:{} =======> 文件上传路径: {}",fileName,fileUrl);
}catch(Exception e){
e.printStackTrace();
map.put("status",false);
map.put("Msg","上传失败!");
}
return map;
}
}
三、前端页面设计
聊天页面
chatIndex.vue
class="scroll-view" :style="{height: `${windowHeight-inputHeight}rpx`}" id="scrollview" scroll-y="true" :scroll-top="scrollTop" @scrolltoupper="topRefresh" @click="touchClose" > {{changeTime(item.sendTime)}} {{item.content}} {{item.content}}
发表评论