3 Commits

Author SHA1 Message Date
KilLze
cf6f9b8b7c 完成消息撤回功能 2026-01-08 01:20:11 +08:00
KilLze
0a17eb8deb 简单优化 2026-01-08 00:07:25 +08:00
KilLze
448ce1d3d6 完成聊天文件上传OSS 2026-01-08 00:06:10 +08:00
10 changed files with 242 additions and 15 deletions

View File

@@ -11,6 +11,7 @@ import com.bao.dating.service.ChatService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -25,6 +26,17 @@ public class ChatController {
@Autowired @Autowired
private ChatService chatService; private ChatService chatService;
/**
* 上传文件
* @param file 文件
* @return 文件URL
*/
@PostMapping("/upload")
public Result<String> uploadVideo(@RequestParam MultipartFile file) {
String url = chatService.uploadChat(file);
return Result.success(ResultCode.SUCCESS, "文件上传成功", url);
}
/** /**
* 获取聊天记录 * 获取聊天记录
* @param targetUserId 目标用户ID * @param targetUserId 目标用户ID

View File

@@ -31,7 +31,6 @@ public class PostController {
* @param files 媒体文件数组 * @param files 媒体文件数组
* @return 上传后的文件URL列表 * @return 上传后的文件URL列表
*/ */
@Log
@PostMapping(value = "/upload", consumes = "multipart/form-data") @PostMapping(value = "/upload", consumes = "multipart/form-data")
public Result<List<String>> uploadMedia(@RequestParam("files") MultipartFile[] files) { public Result<List<String>> uploadMedia(@RequestParam("files") MultipartFile[] files) {
List<String> fileUrls = postService.uploadMedia(files); List<String> fileUrls = postService.uploadMedia(files);
@@ -43,7 +42,6 @@ public class PostController {
* @param postDTO 动态信息 * @param postDTO 动态信息
* @return 发布的动态对象 * @return 发布的动态对象
*/ */
@Log
@PostMapping( "/createPost") @PostMapping( "/createPost")
public Result<Post> createPostJson(@RequestBody PostRequestDTO postDTO) { public Result<Post> createPostJson(@RequestBody PostRequestDTO postDTO) {
// 调用 Service 层处理发布动态业务逻辑 // 调用 Service 层处理发布动态业务逻辑
@@ -57,7 +55,6 @@ public class PostController {
* @param postIds 动态ID * @param postIds 动态ID
* @return 删除结果 * @return 删除结果
*/ */
@Log
@PostMapping("/deletePost") @PostMapping("/deletePost")
public Result<String> deleteById(@RequestBody List<Long> postIds){ public Result<String> deleteById(@RequestBody List<Long> postIds){
int deletedCount = postService.deletePostById(postIds); int deletedCount = postService.deletePostById(postIds);
@@ -81,7 +78,6 @@ public class PostController {
* @param postRequestDTO 动态信息 * @param postRequestDTO 动态信息
* @return 更新后的动态对象 * @return 更新后的动态对象
*/ */
@Log
@PostMapping("/{postId}/updatePost") @PostMapping("/{postId}/updatePost")
public Result<PostEditVO> updatePost(@PathVariable Long postId, @RequestBody PostRequestDTO postRequestDTO) { public Result<PostEditVO> updatePost(@PathVariable Long postId, @RequestBody PostRequestDTO postRequestDTO) {
PostEditVO result = postService.updatePost(postId, postRequestDTO); PostEditVO result = postService.updatePost(postId, postRequestDTO);

View File

@@ -64,7 +64,6 @@ public class UserController {
* @param file 头像文件 * @param file 头像文件
* @return 上传后的文件URL列表 * @return 上传后的文件URL列表
*/ */
@Log
@PostMapping(value = "/info/uploadAvatar", consumes = "multipart/form-data") @PostMapping(value = "/info/uploadAvatar", consumes = "multipart/form-data")
public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) { public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
String fileUrl = userService.uploadAvatar(file); String fileUrl = userService.uploadAvatar(file);
@@ -76,7 +75,6 @@ public class UserController {
* @param file 背景文件 * @param file 背景文件
* @return 上传后的文件URL列表 * @return 上传后的文件URL列表
*/ */
@Log
@PostMapping(value = "/info/uploadBackground", consumes = "multipart/form-data") @PostMapping(value = "/info/uploadBackground", consumes = "multipart/form-data")
public Result<String> uploadBackground(@RequestParam("file") MultipartFile file) { public Result<String> uploadBackground(@RequestParam("file") MultipartFile file) {
String fileUrl = userService.uploadBackground(file); String fileUrl = userService.uploadBackground(file);
@@ -88,7 +86,6 @@ public class UserController {
* @param userInfoUpdateDTO 用户信息更新参数 * @param userInfoUpdateDTO 用户信息更新参数
* @return 更新后的用户信息 * @return 更新后的用户信息
*/ */
@Log
@PostMapping("/info/update") @PostMapping("/info/update")
public Result<UserInfoVO> userInfoUpdate(@RequestBody UserInfoDTO userInfoUpdateDTO) { public Result<UserInfoVO> userInfoUpdate(@RequestBody UserInfoDTO userInfoUpdateDTO) {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();

View File

@@ -1,11 +1,13 @@
package com.bao.dating.controller.websocket; package com.bao.dating.controller.websocket;
import com.bao.dating.message.WsMessage; import com.bao.dating.message.WsMessage;
import com.bao.dating.pojo.dto.ChatRecallDTO;
import com.bao.dating.pojo.dto.ChatRecordSendDTO; import com.bao.dating.pojo.dto.ChatRecordSendDTO;
import com.bao.dating.pojo.vo.ChatRecordsVO; import com.bao.dating.pojo.vo.ChatRecordsVO;
import com.bao.dating.service.ChatService; import com.bao.dating.service.ChatService;
import com.bao.dating.session.WsSessionManager; import com.bao.dating.session.WsSessionManager;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -57,14 +59,23 @@ public class ChatWebSocketHandler extends TextWebSocketHandler {
log.error("WebSocket session 中未找到 userId"); log.error("WebSocket session 中未找到 userId");
return; return;
} }
// 解析消息
WsMessage<ChatRecordSendDTO> wsMessage =
objectMapper.readValue(message.getPayload(),
new TypeReference<WsMessage<ChatRecordSendDTO>>(){});
// 处理私聊消息 JsonNode node = objectMapper.readTree(message.getPayload());
if ("chat".equals(wsMessage.getType())) { String type = node.get("type").asText();
handlePrivateChat(session, senderUserId, wsMessage.getData()); // 根据消息类型解析消息
WsMessage wsMessage = objectMapper.readValue(message.getPayload(), WsMessage.class);
// 先获取消息类型,再根据类型进行相应处理和转换
if ("chat".equals(type)) {
// 处理私聊消息
WsMessage<ChatRecordSendDTO> chatWsMessage =
objectMapper.convertValue(node, new TypeReference<WsMessage<ChatRecordSendDTO>>(){});
handlePrivateChat(session, senderUserId, chatWsMessage.getData());
} else if ("recall".equals(type)) {
// 处理撤回消息
WsMessage<ChatRecallDTO> recallWsMessage =
objectMapper.convertValue(node, new TypeReference<WsMessage<ChatRecallDTO>>(){});
handleRecallMessage(session, senderUserId, recallWsMessage.getData());
} }
} }
@@ -79,6 +90,7 @@ public class ChatWebSocketHandler extends TextWebSocketHandler {
WsMessage<String> errorMsg = new WsMessage<>(); WsMessage<String> errorMsg = new WsMessage<>();
errorMsg.setType("error"); errorMsg.setType("error");
errorMsg.setData("会话已删除,无法发送消息"); errorMsg.setData("会话已删除,无法发送消息");
// 返回错误信息
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(errorMsg))); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(errorMsg)));
return; return;
} }
@@ -97,6 +109,46 @@ public class ChatWebSocketHandler extends TextWebSocketHandler {
} }
} }
/**
* 消息撤回处理
*/
private void handleRecallMessage(WebSocketSession session, Long senderUserId, Object data) throws Exception {
// 转 DTO
ChatRecallDTO dto = objectMapper.convertValue(data, ChatRecallDTO.class);
// 撤回逻辑
boolean success = chatService.recallMessage(senderUserId, dto.getChatId());
// 如果返回false说明消息撤回失败
if (!success) {
WsMessage<String> errorMsg = new WsMessage<>();
errorMsg.setType("error");
errorMsg.setData("撤回失败,消息可能无法撤回或不存在");
// 返回错误信息
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(errorMsg)));
return;
}
// 创建撤回通知消息
WsMessage<ChatRecallDTO> pushMsg = new WsMessage<>();
pushMsg.setType("recall");
pushMsg.setData(dto);
// 通知自己
if (session.isOpen()) {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(pushMsg)));
}
// 通知对方
WebSocketSession receiverSession =
sessionManager.getSession(dto.getReceiverUserId());
if (receiverSession != null && receiverSession.isOpen()) {
receiverSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(pushMsg))
);
}
}
/** /**
* 用户断开连接(下线) * 用户断开连接(下线)

View File

@@ -32,4 +32,20 @@ public interface ChatRecordsMapper {
* @return 影响行数 * @return 影响行数
*/ */
int markMessagesAsRead(ChatMarkReadDTO markReadDTO); int markMessagesAsRead(ChatMarkReadDTO markReadDTO);
/**
* 根据ID查询聊天记录
* @param chatId 聊天记录ID
* @return 聊天记录
*/
ChatRecords selectById(@Param("chatId") Long chatId);
/**
* 撤回聊天记录
* @param chatId 聊天记录ID
* @param senderUserId 发送者ID
* @return 影响行数
*/
int recallMessage(@Param("chatId") Long chatId,
@Param("senderUserId") Long senderUserId);
} }

View File

@@ -0,0 +1,13 @@
package com.bao.dating.pojo.dto;
import lombok.Data;
/**
* 聊天记录撤回参数
* @author lenovo
*/
@Data
public class ChatRecallDTO {
private Long chatId;
private Long receiverUserId;
}

View File

@@ -4,6 +4,7 @@ import com.bao.dating.common.result.PageResult;
import com.bao.dating.pojo.dto.*; import com.bao.dating.pojo.dto.*;
import com.bao.dating.pojo.vo.ChatRecordsVO; import com.bao.dating.pojo.vo.ChatRecordsVO;
import com.bao.dating.pojo.vo.ChatSessionsVO; import com.bao.dating.pojo.vo.ChatSessionsVO;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -21,6 +22,13 @@ public interface ChatService {
*/ */
ChatRecordsVO createSession(Long senderUserId, ChatRecordSendDTO dto); ChatRecordsVO createSession(Long senderUserId, ChatRecordSendDTO dto);
/**
* 上传媒体文件
* @param file 文件
* @return 文件URL列表
*/
String uploadChat(MultipartFile file);
/** /**
* 获取聊天记录 * 获取聊天记录
* @param dto 查询参数 * @param dto 查询参数
@@ -63,4 +71,12 @@ public interface ChatService {
* @param dto 免打扰参数 * @param dto 免打扰参数
*/ */
void updateMuteStatus(Long userId, ChatSessionMuteDTO dto); void updateMuteStatus(Long userId, ChatSessionMuteDTO dto);
/**
* 撤回消息
* @param senderUserId 发送方ID
* @param chatId 聊天记录ID
* @return 撤回结果
*/
boolean recallMessage(Long senderUserId, Long chatId);
} }

View File

@@ -1,5 +1,8 @@
package com.bao.dating.service.impl; package com.bao.dating.service.impl;
import com.bao.dating.common.aliyun.AliOssUtil;
import com.bao.dating.common.result.AliOssResult;
import com.bao.dating.context.UserContext;
import com.bao.dating.mapper.ChatRecordsMapper; import com.bao.dating.mapper.ChatRecordsMapper;
import com.bao.dating.mapper.ChatSessionsMapper; import com.bao.dating.mapper.ChatSessionsMapper;
import com.bao.dating.pojo.dto.*; import com.bao.dating.pojo.dto.*;
@@ -9,15 +12,23 @@ import com.bao.dating.pojo.vo.ChatRecordsVO;
import com.bao.dating.pojo.vo.ChatSessionsVO; import com.bao.dating.pojo.vo.ChatSessionsVO;
import com.bao.dating.service.ChatService; import com.bao.dating.service.ChatService;
import com.bao.dating.service.UserService; import com.bao.dating.service.UserService;
import com.bao.dating.util.FileUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -34,6 +45,9 @@ public class ChatServiceImpl implements ChatService {
@Autowired @Autowired
private ChatSessionsMapper chatSessionsMapper; private ChatSessionsMapper chatSessionsMapper;
@Autowired
private AliOssUtil ossUtil;
@Autowired @Autowired
private UserService userService; private UserService userService;
@@ -112,6 +126,49 @@ public class ChatServiceImpl implements ChatService {
return vo; return vo;
} }
/**
* 上传媒体文件
* @return 上传后的文件URL
*/
@Override
public String uploadChat(MultipartFile file) {
// 参数校验
if (file == null || file.isEmpty()) {
throw new RuntimeException("文件不存在");
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new RuntimeException("文件名非法");
}
String fileType = FileUtil.getFileType(originalFilename);
// 仅支持图片和视频文件上传
if (!AliOssResult.IMAGE.equals(fileType) && !AliOssResult.VIDEO.equals(fileType)) {
throw new RuntimeException("仅支持图片和视频文件上传");
}
//生成 OSS 路径
String extension = FileUtil.getFileExtension(originalFilename);
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + extension;
Long userId = UserContext.getUserId();
String objectKey = "chat/" + userId + "/" + fileName;
try {
byte[] fileBytes = file.getBytes();
String ossUrl = ossUtil.upload(fileBytes, objectKey);
if (ossUrl == null || ossUrl.isEmpty()) {
throw new RuntimeException("图片上传失败");
}
return ossUrl;
} catch (Exception e) {
throw new RuntimeException("上传图片失败", e);
}
}
/** /**
* 获取聊天记录 * 获取聊天记录
* @return 聊天记录列表 * @return 聊天记录列表
@@ -137,6 +194,9 @@ public class ChatServiceImpl implements ChatService {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
/**
* 标记聊天消息为已读
*/
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void markChatMessagesAsRead(Long currentUserId, Long targetUserId) { public void markChatMessagesAsRead(Long currentUserId, Long targetUserId) {
@@ -221,4 +281,45 @@ public class ChatServiceImpl implements ChatService {
dto.getMuteStatus() dto.getMuteStatus()
); );
} }
/**
* 撤回消息
* @param senderUserId 发送者用户ID
* @param chatId 聊天记录ID
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recallMessage(Long senderUserId, Long chatId) {
// 查询聊天记录
ChatRecords record = chatRecordsMapper.selectById(chatId);
// 消息不存在
if (record == null) {
log.info("消息不存在chatId: {}", chatId);
return false;
}
// 只能撤回自己发的
if (!record.getSenderUserId().equals(senderUserId)) {
log.info("不能撤回别人发的消息chatId: {},当前用户: {},消息发送者: {}", chatId, senderUserId, record.getSenderUserId());
return false;
}
// 已撤回或已删除
if (record.getMessageStatus() != 1) {
log.info("消息已撤回或已删除chatId: {},当前状态: {}", chatId, record.getMessageStatus());
return false;
}
// 时间限制2 分钟)
Duration duration = Duration.between(record.getSendTime(), LocalDateTime.now());
if (duration.toMinutes() > 2) {
log.info("消息已超过 2 分钟不能撤回chatId: {},发送时间: {}", chatId, record.getSendTime());
return false;
}
return chatRecordsMapper.recallMessage(chatId, senderUserId) > 0;
}
} }

View File

@@ -64,4 +64,29 @@
AND read_status = 0 AND read_status = 0
AND message_status = 1 AND message_status = 1
</update> </update>
<!-- 根据ID查询聊天记录 -->
<select id="selectById" resultType="com.bao.dating.pojo.entity.ChatRecords">
SELECT
chat_id,
sender_user_id,
receiver_user_id,
send_time,
message_status
FROM chat_records
WHERE chat_id = #{chatId}
</select>
<!-- 撤回消息 -->
<update id="recallMessage">
UPDATE chat_records
SET
message_status = 2,
updated_at = NOW()
WHERE
chat_id = #{chatId}
AND sender_user_id = #{senderUserId}
AND message_status = 1
</update>
</mapper> </mapper>

View File

@@ -57,7 +57,6 @@
last_message_time, unread_count, top_status, mute_status last_message_time, unread_count, top_status, mute_status
FROM chat_sessions FROM chat_sessions
WHERE user_id = #{userId} WHERE user_id = #{userId}
AND session_status = 1
AND session_status in (1,2) AND session_status in (1,2)
ORDER BY top_status DESC, last_message_time DESC ORDER BY top_status DESC, last_message_time DESC
</select> </select>