diff --git a/pom.xml b/pom.xml index 816800b..bf5936f 100644 --- a/pom.xml +++ b/pom.xml @@ -178,6 +178,12 @@ spring-boot-starter-data-redis + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.4.7 + + diff --git a/src/main/java/com/bao/dating/DatingApplication.java b/src/main/java/com/bao/dating/DatingApplication.java index 2b9589b..4237bb4 100644 --- a/src/main/java/com/bao/dating/DatingApplication.java +++ b/src/main/java/com/bao/dating/DatingApplication.java @@ -10,6 +10,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + @MapperScan("com.bao.dating.mapper") @SpringBootApplication @EnableScheduling diff --git a/src/main/java/com/bao/dating/common/result/ChatSendResult.java b/src/main/java/com/bao/dating/common/result/ChatSendResult.java new file mode 100644 index 0000000..cb694bd --- /dev/null +++ b/src/main/java/com/bao/dating/common/result/ChatSendResult.java @@ -0,0 +1,15 @@ +package com.bao.dating.common.result; + +import com.bao.dating.pojo.vo.ChatRecordsVO; +import lombok.Data; + +/** + * 聊天发送结果 + * @author KilLze + */ +@Data +public class ChatSendResult { + private boolean success; + private String message; + private ChatRecordsVO record; +} diff --git a/src/main/java/com/bao/dating/common/result/PageResult.java b/src/main/java/com/bao/dating/common/result/PageResult.java new file mode 100644 index 0000000..f1634ef --- /dev/null +++ b/src/main/java/com/bao/dating/common/result/PageResult.java @@ -0,0 +1,28 @@ +package com.bao.dating.common.result; + +import lombok.Data; + +import java.util.List; + +@Data +public class PageResult { + /** 总记录数 */ + private Long total; + /** 当前页码 */ + private Integer page; + /** 每页大小 */ + private Integer size; + /** 总页数 */ + private Integer pages; + /** 当前页数据 */ + private List records; + + public PageResult(Long total, Integer page, Integer size, List records) { + this.total = total; + this.page = page; + this.size = size; + this.records = records; + // 计算总页数 + this.pages = (int) Math.ceil((double) total / size); + } +} \ No newline at end of file diff --git a/src/main/java/com/bao/dating/config/RedisConfig.java b/src/main/java/com/bao/dating/config/RedisConfig.java index 31e44d9..318fc86 100644 --- a/src/main/java/com/bao/dating/config/RedisConfig.java +++ b/src/main/java/com/bao/dating/config/RedisConfig.java @@ -14,9 +14,9 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 创建RedisTemplate对象 - RedisTemplate redisTemplate = new RedisTemplate<>(); + RedisTemplate redisTemplate = new RedisTemplate<>(); // 设置redis的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); diff --git a/src/main/java/com/bao/dating/config/WebSocketConfig.java b/src/main/java/com/bao/dating/config/WebSocketConfig.java new file mode 100644 index 0000000..67bf631 --- /dev/null +++ b/src/main/java/com/bao/dating/config/WebSocketConfig.java @@ -0,0 +1,35 @@ +package com.bao.dating.config; + + +import com.bao.dating.controller.websocket.ChatWebSocketHandler; +import com.bao.dating.interceptor.WsAuthInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +/** + * WebSocket 配置类 + * @author lenovo + */ +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + @Autowired + private ChatWebSocketHandler chatWebSocketHandler; + + @Autowired + private WsAuthInterceptor wsAuthInterceptor; + + /** + * 注册 WebSocket 处理器 + * @param registry + */ + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(chatWebSocketHandler, "/ws/chat") + .addInterceptors(wsAuthInterceptor) + .setAllowedOrigins("*"); + } +} diff --git a/src/main/java/com/bao/dating/controller/ChatController.java b/src/main/java/com/bao/dating/controller/ChatController.java new file mode 100644 index 0000000..56ae9c4 --- /dev/null +++ b/src/main/java/com/bao/dating/controller/ChatController.java @@ -0,0 +1,104 @@ +package com.bao.dating.controller; + +import com.bao.dating.common.Result; +import com.bao.dating.common.ResultCode; +import com.bao.dating.common.result.PageResult; +import com.bao.dating.context.UserContext; +import com.bao.dating.pojo.dto.*; +import com.bao.dating.pojo.vo.ChatRecordsVO; +import com.bao.dating.pojo.vo.ChatSessionsVO; +import com.bao.dating.service.ChatService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 聊天控制器 + * @author lenovo + */ +@Slf4j +@RestController +@RequestMapping("/chat") +public class ChatController { + @Autowired + private ChatService chatService; + + /** + * 上传文件 + * @param file 文件 + * @return 文件URL + */ + @PostMapping("/upload") + public Result uploadVideo(@RequestParam MultipartFile file) { + String url = chatService.uploadChat(file); + return Result.success(ResultCode.SUCCESS, "文件上传成功", url); + } + + /** + * 获取聊天记录 + * @param targetUserId 目标用户ID + * @param pageDTO 分页参数 + * @return 聊天记录列表 + */ + @GetMapping("/history/{targetUserId}") + public Result> getChatHistory( + @PathVariable Long targetUserId, + ChatCursorPageDTO pageDTO){ + Long currentUserId = UserContext.getUserId(); + ChatHistoryQueryDTO queryDTO = new ChatHistoryQueryDTO(); + queryDTO.setCurrentUserId(currentUserId); + queryDTO.setTargetUserId(targetUserId); + queryDTO.setCursor(pageDTO.getCursor()); + queryDTO.setSize(pageDTO.getSize()); + chatService.markChatMessagesAsRead(currentUserId, targetUserId); + List history = chatService.getChatHistory(queryDTO); + return Result.success(ResultCode.SUCCESS, "获取聊天记录成功", history); + } + + /** + * 获取会话列表 + * @return 会话列表 + */ + @GetMapping("/sessions") + public Result> getSessionList() { + Long currentUserId = UserContext.getUserId(); + List list = chatService.getSessionList(currentUserId); + return Result.success(ResultCode.SUCCESS, "获取会话列表成功", list); + } + + /** + * 更新会话状态 + * @param dto 会话状态 + * @return 无 + */ + @PostMapping("/session/status") + public Result updateSessionStatus(@RequestBody ChatSessionStatusDTO dto) { + chatService.updateSessionStatus(UserContext.getUserId(), dto); + return Result.success(ResultCode.SUCCESS, null); + } + + /** + * 置顶会话 + * @param dto 置顶状态 + * @return 无 + */ + @PostMapping("/session/top") + public Result updateTopStatus(@RequestBody ChatSessionTopDTO dto) { + chatService.updateTopStatus(UserContext.getUserId(), dto); + return Result.success(ResultCode.SUCCESS, null); + } + + /** + * 静音会话 + * @param dto 静音状态 + * @return 无 + */ + @PostMapping("/session/mute") + public Result updateMuteStatus(@RequestBody ChatSessionMuteDTO dto) { + chatService.updateMuteStatus(UserContext.getUserId(), dto); + return Result.success(ResultCode.SUCCESS, null); + } +} diff --git a/src/main/java/com/bao/dating/controller/PostController.java b/src/main/java/com/bao/dating/controller/PostController.java index b4fb20b..799e9eb 100644 --- a/src/main/java/com/bao/dating/controller/PostController.java +++ b/src/main/java/com/bao/dating/controller/PostController.java @@ -31,7 +31,6 @@ public class PostController { * @param files 媒体文件数组 * @return 上传后的文件URL列表 */ - @Log @PostMapping(value = "/upload", consumes = "multipart/form-data") public Result> uploadMedia(@RequestParam("files") MultipartFile[] files) { List fileUrls = postService.uploadMedia(files); @@ -43,7 +42,6 @@ public class PostController { * @param postDTO 动态信息 * @return 发布的动态对象 */ - @Log @PostMapping( "/createPost") public Result createPostJson(@RequestBody PostRequestDTO postDTO) { // 调用 Service 层处理发布动态业务逻辑 @@ -57,7 +55,6 @@ public class PostController { * @param postIds 动态ID * @return 删除结果 */ - @Log @PostMapping("/deletePost") public Result deleteById(@RequestBody List postIds){ int deletedCount = postService.deletePostById(postIds); @@ -81,7 +78,6 @@ public class PostController { * @param postRequestDTO 动态信息 * @return 更新后的动态对象 */ - @Log @PostMapping("/{postId}/updatePost") public Result updatePost(@PathVariable Long postId, @RequestBody PostRequestDTO postRequestDTO) { PostEditVO result = postService.updatePost(postId, postRequestDTO); diff --git a/src/main/java/com/bao/dating/controller/UserController.java b/src/main/java/com/bao/dating/controller/UserController.java index 8e6db82..ac6b4af 100644 --- a/src/main/java/com/bao/dating/controller/UserController.java +++ b/src/main/java/com/bao/dating/controller/UserController.java @@ -4,12 +4,11 @@ import com.bao.dating.anno.Log; import com.bao.dating.common.Result; import com.bao.dating.common.ResultCode; import com.bao.dating.context.UserContext; -import com.bao.dating.pojo.dto.UserInfoUpdateDTO; +import com.bao.dating.pojo.dto.UserInfoDTO; import com.bao.dating.pojo.dto.UserLoginDTO; import com.bao.dating.pojo.vo.UserInfoVO; import com.bao.dating.pojo.vo.UserLoginVO; import com.bao.dating.service.UserService; -import io.jsonwebtoken.Jwt; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -65,7 +64,6 @@ public class UserController { * @param file 头像文件 * @return 上传后的文件URL列表 */ - @Log @PostMapping(value = "/info/uploadAvatar", consumes = "multipart/form-data") public Result uploadAvatar(@RequestParam("file") MultipartFile file) { String fileUrl = userService.uploadAvatar(file); @@ -77,7 +75,6 @@ public class UserController { * @param file 背景文件 * @return 上传后的文件URL列表 */ - @Log @PostMapping(value = "/info/uploadBackground", consumes = "multipart/form-data") public Result uploadBackground(@RequestParam("file") MultipartFile file) { String fileUrl = userService.uploadBackground(file); @@ -89,9 +86,8 @@ public class UserController { * @param userInfoUpdateDTO 用户信息更新参数 * @return 更新后的用户信息 */ - @Log @PostMapping("/info/update") - public Result userInfoUpdate(@RequestBody UserInfoUpdateDTO userInfoUpdateDTO) { + public Result userInfoUpdate(@RequestBody UserInfoDTO userInfoUpdateDTO) { Long userId = UserContext.getUserId(); userInfoUpdateDTO.setUserId(userId); UserInfoVO userInfoVO =userService.updateUserInfo(userInfoUpdateDTO); diff --git a/src/main/java/com/bao/dating/controller/websocket/ChatWebSocketHandler.java b/src/main/java/com/bao/dating/controller/websocket/ChatWebSocketHandler.java new file mode 100644 index 0000000..ed8cc43 --- /dev/null +++ b/src/main/java/com/bao/dating/controller/websocket/ChatWebSocketHandler.java @@ -0,0 +1,165 @@ +package com.bao.dating.controller.websocket; + +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.vo.ChatRecordsVO; +import com.bao.dating.service.ChatService; +import com.bao.dating.session.WsSessionManager; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * WebSocket处理器类 + * @author lenovo + */ +@Slf4j +@Component +public class ChatWebSocketHandler extends TextWebSocketHandler { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ChatService chatService; + + @Autowired + private WsSessionManager sessionManager; + + + /** + * 用户建立连接(上线) + * @param session WebSocketSession + */ + @Override + public void afterConnectionEstablished(WebSocketSession session) { + Long userId = (Long) session.getAttributes().get("userId"); + sessionManager.addSession(userId, session); + log.info("用户 " + userId + " 已上线"); + } + + /** + * 接收并处理消息 + * @param session WebSocketSession + * @param message 消息 + */ + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception{ + // 获取当前用户ID + Long senderUserId = (Long) session.getAttributes().get("userId"); + if (senderUserId == null) { + log.error("WebSocket session 中未找到 userId"); + return; + } + + JsonNode node = objectMapper.readTree(message.getPayload()); + String type = node.get("type").asText(); + // 根据消息类型解析消息 + WsMessage wsMessage = objectMapper.readValue(message.getPayload(), WsMessage.class); + + // 先获取消息类型,再根据类型进行相应处理和转换 + if ("chat".equals(type)) { + // 处理私聊消息 + WsMessage chatWsMessage = + objectMapper.convertValue(node, new TypeReference>(){}); + handlePrivateChat(session, senderUserId, chatWsMessage.getData()); + } else if ("recall".equals(type)) { + // 处理撤回消息 + WsMessage recallWsMessage = + objectMapper.convertValue(node, new TypeReference>(){}); + handleRecallMessage(session, senderUserId, recallWsMessage.getData()); + } + } + + /** + * 私聊处理 + */ + private void handlePrivateChat(WebSocketSession session, Long senderUserId, ChatRecordSendDTO dto) throws Exception { + + // 1. 消息入库 + 会话更新 + ChatRecordsVO chatRecordsVO = chatService.createSession(senderUserId, dto); + if (chatRecordsVO == null){ + WsMessage errorMsg = new WsMessage<>(); + errorMsg.setType("error"); + errorMsg.setData("会话已删除,无法发送消息"); + // 返回错误信息 + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(errorMsg))); + return; + } + // 2. 推送给接收方 + WebSocketSession receiverSession = + sessionManager.getSession(dto.getReceiverUserId()); + + if (receiverSession != null && receiverSession.isOpen()) { + WsMessage pushMsg = new WsMessage<>(); + pushMsg.setType("chat"); + pushMsg.setData(chatRecordsVO); + + receiverSession.sendMessage( + new TextMessage(objectMapper.writeValueAsString(pushMsg)) + ); + } + } + + /** + * 消息撤回处理 + */ + 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 errorMsg = new WsMessage<>(); + errorMsg.setType("error"); + errorMsg.setData("撤回失败,消息可能无法撤回或不存在"); + // 返回错误信息 + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(errorMsg))); + return; + } + + // 创建撤回通知消息 + WsMessage 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)) + ); + } + } + + + /** + * 用户断开连接(下线) + * @param session WebSocketSession + * @param status 断开原因 + */ + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status){ + // 下线处理 + Long userId = (Long) session.getAttributes().get("userId"); + sessionManager.removeSession(userId); + log.info("用户 " + userId + " 已下线"); + } +} \ No newline at end of file diff --git a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java index 8e11f59..3203fc9 100644 --- a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java +++ b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java @@ -23,7 +23,7 @@ import org.springframework.web.servlet.HandlerInterceptor; public class TokenInterceptor implements HandlerInterceptor { @Autowired - private RedisTemplate redisTemplate; + private RedisTemplate redisTemplate; /** * 在请求处理之前进行拦截 @@ -68,7 +68,7 @@ public class TokenInterceptor implements HandlerInterceptor { } // 解析 token - String userId = JwtUtil.getSubjectFromToken(token); + Long userId = Long.valueOf(JwtUtil.getSubjectFromToken(token)); // 从Redis获取存储的token进行比对 Object redisTokenObj = redisTemplate.opsForValue().get("login:token:" + userId); @@ -85,7 +85,7 @@ public class TokenInterceptor implements HandlerInterceptor { log.info("用户: {}", userId); // 保存 userId 到 ThreadLocal - UserContext.setUserId(Long.valueOf(userId)); + UserContext.setUserId(userId); return true; } catch (Exception e) { log.error("Token 校验失败: {}", e.getMessage()); diff --git a/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java new file mode 100644 index 0000000..a01153c --- /dev/null +++ b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java @@ -0,0 +1,111 @@ +package com.bao.dating.interceptor; + +import com.bao.dating.util.JwtUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * WebSocket 认证拦截器 + */ +@Slf4j +@Component +public class WsAuthInterceptor implements HandshakeInterceptor { + + @Autowired + private RedisTemplate redisTemplate; + /** + * 拦截WebSocket连接请求 + */ + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { + log.info("开始WebSocket握手认证"); + + // 获取请求参数 + if (!(request instanceof ServletServerHttpRequest)) { + log.error("WebSocket握手失败:非HTTP请求"); + return false; + } + + // 获取HttpServletRequest对象 + HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + + // 从URL参数中获取token + String token = servletRequest.getParameter("token"); + + if (StringUtils.isBlank(token)) { + log.error("WebSocket握手失败:令牌丢失"); + return false; + } + + try { + // 验证 token 是否有效(包括是否过期) + if (!JwtUtil.validateToken(token)) { + log.error("Token无效或已过期: {}", token); + return false; + } + + // 检查 token 是否在黑名单中 + String blacklistKey = "jwt:blacklist:" + token; + Object blacklistToken = redisTemplate.opsForValue().get(blacklistKey); + if (blacklistToken != null) { + log.error("Token已在黑名单中: {}", token); + return false; + } + + // 验证token并获取用户ID + String userIdStr = JwtUtil.getSubjectFromToken(token); + + if (StringUtils.isBlank(userIdStr) || !userIdStr.matches("\\d+")) { + log.error("无效的用户ID格式: {}", userIdStr); + return false; + } + + Long userId = Long.valueOf(userIdStr); + + // 从Redis获取存储的token进行比对 + String redisTokenKey = "login:token:" + userId; + Object redisTokenObj = redisTemplate.opsForValue().get(redisTokenKey); + String redisToken = redisTokenObj != null ? redisTokenObj.toString() : null; + log.info("Redis中存储的token: {}", redisToken != null ? "存在" : "不存在"); + + // 验证Redis中的token是否存在且匹配 + if (redisToken == null || !redisToken.equals(token)) { + log.error("登录已失效 - Redis中token不存在或不匹配"); + return false; + } + + log.info("WebSocket认证成功,用户ID: {}", userId); + // 将用户ID保存到attributes中 + attributes.put("userId", userId); + return true; + } + catch (NumberFormatException e) { + log.error("用户ID格式转换异常: {}", e.getMessage()); + return false; + } + catch (Exception e) { + log.error("WebSocket握手失败:{}", e.getMessage(), e); + return false; + } + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + if (exception != null) { + log.error("WebSocket握手后出现异常:", exception); + } else { + log.info("WebSocket握手完成"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bao/dating/mapper/ChatRecordsMapper.java b/src/main/java/com/bao/dating/mapper/ChatRecordsMapper.java new file mode 100644 index 0000000..630b5a6 --- /dev/null +++ b/src/main/java/com/bao/dating/mapper/ChatRecordsMapper.java @@ -0,0 +1,51 @@ +package com.bao.dating.mapper; + + +import com.bao.dating.pojo.dto.ChatHistoryQueryDTO; +import com.bao.dating.pojo.dto.ChatMarkReadDTO; +import com.bao.dating.pojo.entity.ChatRecords; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface ChatRecordsMapper { + /** + * 插入聊天记录 + * @param chatRecords 聊天记录 + * @return 影响行数 + */ + int insert(ChatRecords chatRecords); + + /** + * 根据时间游标查询聊天记录 + * @param queryDTO 查询参数 + * @return 聊天记录列表 + */ + List selectChatHistoryByCursor(ChatHistoryQueryDTO queryDTO); + + /** + * 标记聊天记录为已读 + * @param markReadDTO 标记参数 + * @return 影响行数 + */ + 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); +} diff --git a/src/main/java/com/bao/dating/mapper/ChatSessionsMapper.java b/src/main/java/com/bao/dating/mapper/ChatSessionsMapper.java new file mode 100644 index 0000000..5647550 --- /dev/null +++ b/src/main/java/com/bao/dating/mapper/ChatSessionsMapper.java @@ -0,0 +1,88 @@ +package com.bao.dating.mapper; + + +import com.bao.dating.pojo.entity.ChatRecords; +import com.bao.dating.pojo.entity.ChatSessions; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface ChatSessionsMapper { + + /** + * 如果发送方存在会话则更新,不存在则创建 + * @param chatSessions 会话 + * @return 影响行数 + */ + int upsertSessionForSender(ChatSessions chatSessions); + + /** + * 获取会话 + * @param userId 用户ID + * @param targetUserId 目标用户ID + * @return 会话 + */ + @Select("SELECT * FROM chat_sessions WHERE user_id = #{userId} AND target_user_id = #{targetUserId} LIMIT 1") + ChatSessions getSession(@Param("userId") Long userId, @Param("targetUserId") Long targetUserId); + + /** + * 如果接收方存在会话则更新,不存在则创建 + * @param chatSessions 会话 + * @return 影响行数 + */ + int upsertSessionForReceiver(ChatSessions chatSessions); + + /** + * 清空会话的未读数 + * @param userId 用户ID + * @param targetUserId 目标用户ID + * @return 影响行数 + */ + int clearUnreadCount( + @Param("userId") Long userId, + @Param("targetUserId") Long targetUserId + ); + + /** + * 查询当前用户的会话列表 + * @param userId 当前用户ID + * @return 会话列表 + */ + List selectSessionsByUserId(@Param("userId") Long userId); + + /** + * 更新会话状态 + * @param userId 用户ID + * @param targetUserId 目标用户ID + * @param sessionStatus 会话状态 + * @return 影响行数 + */ + int updateSessionStatus(@Param("userId") Long userId, + @Param("targetUserId") Long targetUserId, + @Param("sessionStatus") Integer sessionStatus); + + /** + * 更新会话置顶状态 + * @param userId 用户ID + * @param targetUserId 目标用户ID + * @param topStatus 置顶状态 + * @return 影响行数 + */ + int updateTopStatus(@Param("userId") Long userId, + @Param("targetUserId") Long targetUserId, + @Param("topStatus") Integer topStatus); + + /** + * 更新会话免打扰状态 + * @param userId 用户ID + * @param targetUserId 目标用户ID + * @param muteStatus 免打扰状态 + * @return 影响行数 + */ + int updateMuteStatus(@Param("userId") Long userId, + @Param("targetUserId") Long targetUserId, + @Param("muteStatus") Integer muteStatus); +} diff --git a/src/main/java/com/bao/dating/mapper/UserMapper.java b/src/main/java/com/bao/dating/mapper/UserMapper.java index 1313fa7..260fdd8 100644 --- a/src/main/java/com/bao/dating/mapper/UserMapper.java +++ b/src/main/java/com/bao/dating/mapper/UserMapper.java @@ -1,6 +1,6 @@ package com.bao.dating.mapper; -import com.bao.dating.pojo.dto.UserInfoUpdateDTO; +import com.bao.dating.pojo.dto.UserInfoDTO; import com.bao.dating.pojo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -32,7 +32,7 @@ public interface UserMapper { * 更新用户信息 * @param userInfoUpdateDTO 用户信息更新参数 */ - void updateUserInfoByUserId(UserInfoUpdateDTO userInfoUpdateDTO); + void updateUserInfoByUserId(UserInfoDTO userInfoUpdateDTO); /** * 添加用户 diff --git a/src/main/java/com/bao/dating/message/WsMessage.java b/src/main/java/com/bao/dating/message/WsMessage.java new file mode 100644 index 0000000..a9df3fb --- /dev/null +++ b/src/main/java/com/bao/dating/message/WsMessage.java @@ -0,0 +1,17 @@ +package com.bao.dating.message; + +import lombok.Data; + +/** + * WebSocket 消息 + * @author KilLze + */ +@Data +public class WsMessage { + + /** 消息类型:chat / read / system */ + private String type; + + /** 消息体 */ + private T data; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatCursorPageDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatCursorPageDTO.java new file mode 100644 index 0000000..08fa92c --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatCursorPageDTO.java @@ -0,0 +1,25 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 聊天记录分页参数 + * @author lenovo + */ +@Data +public class ChatCursorPageDTO { + + /** + * 时间游标:最后一条消息的发送时间 + */ + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime cursor; + + /** + * 拉取条数 + */ + private Integer size; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatHistoryQueryDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatHistoryQueryDTO.java new file mode 100644 index 0000000..f02b7e3 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatHistoryQueryDTO.java @@ -0,0 +1,17 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 聊天记录查询参数 + * @author KilLze + */ +@Data +public class ChatHistoryQueryDTO { + private Long currentUserId; + private Long targetUserId; + private LocalDateTime cursor; + private Integer size; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatMarkReadDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatMarkReadDTO.java new file mode 100644 index 0000000..5308a68 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatMarkReadDTO.java @@ -0,0 +1,16 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 聊天记录已读参数 + * @author lenovo + */ +@Data +public class ChatMarkReadDTO { + private Long senderUserId; + private Long receiverUserId; + private LocalDateTime readTime; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatRecallDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatRecallDTO.java new file mode 100644 index 0000000..12cfb1d --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatRecallDTO.java @@ -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; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatRecordSendDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatRecordSendDTO.java new file mode 100644 index 0000000..68e8efd --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatRecordSendDTO.java @@ -0,0 +1,17 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +/** + * 聊天记录发送数据传输对象 + * @author KilLze + */ +@Data +public class ChatRecordSendDTO { + /** 接收者用户ID */ + private Long receiverUserId; + /** 消息内容 */ + private String messageContent; + /** 消息类型 (1-文本消息,2-文件消息) */ + private Integer messageType; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatSessionMuteDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatSessionMuteDTO.java new file mode 100644 index 0000000..b932220 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatSessionMuteDTO.java @@ -0,0 +1,13 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +/** + * 聊天会话静音参数 + * @author lenovo + */ +@Data +public class ChatSessionMuteDTO { + private Long targetUserId; + private Integer muteStatus; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatSessionStatusDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatSessionStatusDTO.java new file mode 100644 index 0000000..b637c39 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatSessionStatusDTO.java @@ -0,0 +1,13 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +/** + * 会话状态参数 + * @author lenovo + */ +@Data +public class ChatSessionStatusDTO { + private Long targetUserId; + private Integer sessionStatus; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/ChatSessionTopDTO.java b/src/main/java/com/bao/dating/pojo/dto/ChatSessionTopDTO.java new file mode 100644 index 0000000..2cbdf18 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/ChatSessionTopDTO.java @@ -0,0 +1,13 @@ +package com.bao.dating.pojo.dto; + +import lombok.Data; + +/** + * 会话置顶参数 + * @author lenovo + */ +@Data +public class ChatSessionTopDTO { + private Long targetUserId; + private Integer topStatus; +} diff --git a/src/main/java/com/bao/dating/pojo/dto/UserInfoUpdateDTO.java b/src/main/java/com/bao/dating/pojo/dto/UserInfoDTO.java similarity index 84% rename from src/main/java/com/bao/dating/pojo/dto/UserInfoUpdateDTO.java rename to src/main/java/com/bao/dating/pojo/dto/UserInfoDTO.java index a3bc0ab..3d11b03 100644 --- a/src/main/java/com/bao/dating/pojo/dto/UserInfoUpdateDTO.java +++ b/src/main/java/com/bao/dating/pojo/dto/UserInfoDTO.java @@ -8,11 +8,11 @@ import java.time.LocalDateTime; import java.util.List; /** - * 用户信息更新数据传输对象 + * 用户信息数据传输对象 * @author KilLze */ @Data -public class UserInfoUpdateDTO implements Serializable { +public class UserInfoDTO implements Serializable { private Long userId; private String userName; private String nickname; diff --git a/src/main/java/com/bao/dating/pojo/entity/ChatRecords.java b/src/main/java/com/bao/dating/pojo/entity/ChatRecords.java new file mode 100644 index 0000000..6bd8237 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/entity/ChatRecords.java @@ -0,0 +1,35 @@ +package com.bao.dating.pojo.entity; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 聊天记录表 + * @author lenovo + */ +@Data +public class ChatRecords { + /** 聊天记录ID */ + private Long chatId; + /** 发送者用户ID */ + private Long senderUserId; + /** 接收者用户ID */ + private Long receiverUserId; + /** 消息内容 */ + private String messageContent; + /** 消息类型 (1-文本消息,2-文件消息) */ + private Integer messageType; + /** 阅读状态 (0-未读,1-已读) */ + private Integer readStatus; + /** 阅读时间 */ + private LocalDateTime readTime; + /** 发送时间 */ + private LocalDateTime sendTime; + /** 消息状态 (1-正常,2-已撤回,3-已删除) */ + private Integer messageStatus; + /** 创建时间 */ + private LocalDateTime createdAt; + /** 更新时间 */ + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/bao/dating/pojo/entity/ChatSessions.java b/src/main/java/com/bao/dating/pojo/entity/ChatSessions.java new file mode 100644 index 0000000..5b85c72 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/entity/ChatSessions.java @@ -0,0 +1,40 @@ +package com.bao.dating.pojo.entity; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会话表 + * @author KilLze + */ +@Data +public class ChatSessions { + /** 会话ID */ + private Long sessionId; + /** 所属用户ID */ + private Long userId; + /** 目标用户ID */ + private Long targetUserId; + /** 会话名称 */ + private String sessionName; + /** 最后一条消息ID (关联chat_records.chat_id) */ + private Long lastMessageId; + /** 最后一条消息内容 */ + private String lastMessageContent; + /** 最后一条消息时间 */ + private LocalDateTime lastMessageTime; + /** 未读消息数量 */ + private Integer unreadCount; + /** 会话状态 (1-正常,2-已隐藏,3-已删除) */ + private Integer sessionStatus; + /** 置顶状态 (0-未置顶,1-已置顶) */ + private Integer topStatus; + /** 免打扰状态 (0-正常提醒,1-免打扰) */ + private Integer muteStatus; + /** 创建时间 */ + private LocalDateTime createdAt; + /** 更新时间 */ + private LocalDateTime updatedAt; + +} diff --git a/src/main/java/com/bao/dating/pojo/vo/ChatRecordsVO.java b/src/main/java/com/bao/dating/pojo/vo/ChatRecordsVO.java new file mode 100644 index 0000000..deb9f92 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/vo/ChatRecordsVO.java @@ -0,0 +1,29 @@ +package com.bao.dating.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 聊天记录返回数据 + * @author KilLze + */ +@Data +public class ChatRecordsVO { + /** 聊天记录ID */ + private Long chatId; + /** 发送者用户ID */ + private Long senderUserId; + /** 接收者用户ID */ + private Long receiverUserId; + /** 消息内容 */ + private String messageContent; + /** 消息类型 (1-文本消息,2-文件消息) */ + private Integer messageType; + /** 阅读状态 (0-未读,1-已读) */ + private Integer readStatus; + /** 发送时间 */ + private LocalDateTime sendTime; + /** 消息状态 (1-正常,2-已撤回,3-已删除) */ + private Integer messageStatus; +} diff --git a/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java b/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java new file mode 100644 index 0000000..3e28e5b --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java @@ -0,0 +1,32 @@ +package com.bao.dating.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会话信息 + * @author KilLze + */ +@Data +public class ChatSessionsVO { + + /** 会话ID */ + private Long sessionId; + /** 对方用户ID */ + private Long targetUserId; + /** 会话名称 */ + private String sessionName; + /** 对方用户头像 */ + private String avatarUrl; + /** 最后一条消息内容 */ + private String lastMessageContent; + /** 最后一条消息时间 */ + private LocalDateTime lastMessageTime; + /** 未读消息数量 */ + private Integer unreadCount; + /** 置顶状态 */ + private Integer topStatus; + /** 免打扰状态 */ + private Integer muteStatus; +} diff --git a/src/main/java/com/bao/dating/service/ChatService.java b/src/main/java/com/bao/dating/service/ChatService.java new file mode 100644 index 0000000..8cc4cf1 --- /dev/null +++ b/src/main/java/com/bao/dating/service/ChatService.java @@ -0,0 +1,82 @@ +package com.bao.dating.service; + +import com.bao.dating.common.result.PageResult; +import com.bao.dating.pojo.dto.*; +import com.bao.dating.pojo.vo.ChatRecordsVO; +import com.bao.dating.pojo.vo.ChatSessionsVO; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 聊天服务 + * @author lenovo + */ +public interface ChatService { + /** + * 消息入库,如果会话不存在则创建会话 + * @param senderUserId 发送方用户ID + * @param dto 发送参数 + * @return 聊天记录VO + */ + ChatRecordsVO createSession(Long senderUserId, ChatRecordSendDTO dto); + + /** + * 上传媒体文件 + * @param file 文件 + * @return 文件URL列表 + */ + String uploadChat(MultipartFile file); + + /** + * 获取聊天记录 + * @param dto 查询参数 + * @return 聊天记录列表 + */ + List getChatHistory(ChatHistoryQueryDTO dto); + + + /** + * 标记聊天记录为已读 + * @param currentUserId 当前用户ID + * @param targetUserId 目标用户ID + */ + void markChatMessagesAsRead(Long currentUserId, Long targetUserId); + + /** + * 获取会话列表 + * @param currentUserId 当前用户ID + * @return 会话列表 + */ + List getSessionList(Long currentUserId); + + /** + * 更新会话状态 + * @param userId 用户ID + * @param dto 更新参数 + */ + void updateSessionStatus(Long userId, ChatSessionStatusDTO dto); + + /** + * 置顶会话 + * @param userId 用户ID + * @param dto 置顶参数 + */ + void updateTopStatus(Long userId, ChatSessionTopDTO dto); + + /** + * 免打扰会话 + * @param userId 用户ID + * @param dto 免打扰参数 + */ + void updateMuteStatus(Long userId, ChatSessionMuteDTO dto); + + /** + * 撤回消息 + * @param senderUserId 发送方ID + * @param chatId 聊天记录ID + * @return 撤回结果 + */ + boolean recallMessage(Long senderUserId, Long chatId); +} diff --git a/src/main/java/com/bao/dating/service/UserService.java b/src/main/java/com/bao/dating/service/UserService.java index 252991e..5b7b67c 100644 --- a/src/main/java/com/bao/dating/service/UserService.java +++ b/src/main/java/com/bao/dating/service/UserService.java @@ -1,6 +1,6 @@ package com.bao.dating.service; -import com.bao.dating.pojo.dto.UserInfoUpdateDTO; +import com.bao.dating.pojo.dto.UserInfoDTO; import com.bao.dating.pojo.dto.UserLoginDTO; import com.bao.dating.pojo.entity.User; import com.bao.dating.pojo.vo.UserInfoVO; @@ -52,20 +52,28 @@ public interface UserService { * @param userInfoUpdateDTO 用户信息 * @return 更新后的用户信息 */ - UserInfoVO updateUserInfo(UserInfoUpdateDTO userInfoUpdateDTO); + UserInfoVO updateUserInfo(UserInfoDTO userInfoUpdateDTO); /** * 用户注册 - * @param userName 用户民称 + * @param userName 用户名称 + * @param userPassword 用户密码 * @return 用户信息 */ - Boolean registerUser(String userName,String userPassword); + Boolean registerUser(String userName, String userPassword); /** * 邮箱登录 * @param email 邮箱 * @param code 验证码 - * @return + * @return 登录结果 */ - UserLoginVO emailLogin(String email , String code); + UserLoginVO emailLogin(String email, String code); + + /** + * 根据用户ID查询用户信息 + * @param userId 用户ID + * @return 用户 + */ + UserInfoDTO getUserInfoById(Long userId); } diff --git a/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java b/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..517eb9c --- /dev/null +++ b/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java @@ -0,0 +1,325 @@ +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.ChatSessionsMapper; +import com.bao.dating.pojo.dto.*; +import com.bao.dating.pojo.entity.ChatRecords; +import com.bao.dating.pojo.entity.ChatSessions; +import com.bao.dating.pojo.vo.ChatRecordsVO; +import com.bao.dating.pojo.vo.ChatSessionsVO; +import com.bao.dating.service.ChatService; +import com.bao.dating.service.UserService; +import com.bao.dating.util.FileUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +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.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + + +/** + * 聊天服务实现类 + * @author lenovo + */ +@Slf4j +@Service +public class ChatServiceImpl implements ChatService { + @Autowired + private ChatRecordsMapper chatRecordsMapper; + + @Autowired + private ChatSessionsMapper chatSessionsMapper; + + @Autowired + private AliOssUtil ossUtil; + + @Autowired + private UserService userService; + + /** + * 消息入库,如果会话不存在则创建会话 + * @param senderUserId 发送者用户ID + * @param dto 消息 + * @return 聊天记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ChatRecordsVO createSession(Long senderUserId, ChatRecordSendDTO dto) { + + ChatSessions session = chatSessionsMapper.getSession(senderUserId, dto.getReceiverUserId()); + if (session != null && session.getSessionStatus() == 3) { + log.warn("会话已删除,无法发送消息"); + return null; + } + + ChatRecords record = new ChatRecords(); + record.setSenderUserId(senderUserId); + record.setReceiverUserId(dto.getReceiverUserId()); + record.setMessageContent(dto.getMessageContent()); + record.setMessageType(dto.getMessageType()); + record.setReadStatus(0); + record.setMessageStatus(1); + record.setSendTime(LocalDateTime.now()); + + // 插入消息记录 + chatRecordsMapper.insert(record); + + // 创建发送方会话 + ChatSessions sessions = new ChatSessions(); + sessions.setUserId(senderUserId); + sessions.setTargetUserId(dto.getReceiverUserId()); + // 获取接收方昵称作为发送方会话名称 + try { + UserInfoDTO receiverNicknameInfo = userService.getUserInfoById(dto.getReceiverUserId()); + if (receiverNicknameInfo != null && receiverNicknameInfo.getNickname() != null) { + sessions.setSessionName(receiverNicknameInfo.getNickname()); + } else { + log.warn("接收用户ID获取用户信息时出现错误: {} , 使用默认id", dto.getReceiverUserId()); + sessions.setSessionName("用户" + dto.getReceiverUserId()); + } + } catch (Exception e) { + log.error("接收用户ID获取用户信息时出现错误: {} , 使用默认id", dto.getReceiverUserId(), e); + sessions.setSessionName("用户" + dto.getReceiverUserId()); + } + sessions.setLastMessageId(record.getChatId()); + sessions.setLastMessageContent(record.getMessageContent()); + sessions.setLastMessageTime(record.getSendTime()); + + chatSessionsMapper.upsertSessionForSender(sessions); + + // 创建接收方会话 + sessions.setUserId(dto.getReceiverUserId()); + sessions.setTargetUserId(senderUserId); + try { + UserInfoDTO senderNicknameInfo = userService.getUserInfoById(senderUserId); + if (senderNicknameInfo != null && senderNicknameInfo.getNickname() != null) { + sessions.setSessionName(senderNicknameInfo.getNickname()); + } else { + log.warn("接收用户ID获取用户信息时出现错误: {} , 使用默认id", senderUserId); + sessions.setSessionName("用户" + senderUserId); + } + } catch (Exception e) { + log.error("接收用户ID获取用户信息时出现错误: {} , 使用默认id", senderUserId, e); + sessions.setSessionName("用户" + senderUserId); + } + + chatSessionsMapper.upsertSessionForReceiver(sessions); + + // 3. 返回 VO + ChatRecordsVO vo = new ChatRecordsVO(); + BeanUtils.copyProperties(record, 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 聊天记录列表 + */ + @Override + public List getChatHistory(ChatHistoryQueryDTO dto) { + + Integer size = dto.getSize(); + if (size == null || size < 1 || size > 100) { + dto.setSize(50); + } + + // 查询聊天记录 + List records = chatRecordsMapper.selectChatHistoryByCursor(dto); + + // 倒序 + Collections.reverse(records); + + return records.stream().map(record -> { + ChatRecordsVO vo = new ChatRecordsVO(); + BeanUtils.copyProperties(record, vo); + return vo; + }).collect(Collectors.toList()); + } + + /** + * 标记聊天消息为已读 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void markChatMessagesAsRead(Long currentUserId, Long targetUserId) { + // 更新 chat_records:把对方发给我的未读消息设为已读 + ChatMarkReadDTO markReadDTO = new ChatMarkReadDTO(); + // 因为读的是对方发给我的消息所以要把填入的参数反过来 + markReadDTO.setSenderUserId(targetUserId); + markReadDTO.setReceiverUserId(currentUserId); + markReadDTO.setReadTime(LocalDateTime.now()); + + chatRecordsMapper.markMessagesAsRead(markReadDTO); + + // 更新 chat_sessions:清空当前会话的未读数 + chatSessionsMapper.clearUnreadCount( + currentUserId, + targetUserId + ); + } + + /** + * 获取会话列表 + * @return 会话列表 + */ + @Override + public List getSessionList(Long currentUserId) { + List sessions = chatSessionsMapper.selectSessionsByUserId(currentUserId); + + return sessions.stream().map(session -> { + ChatSessionsVO vo = new ChatSessionsVO(); + BeanUtils.copyProperties(session, vo); + + UserInfoDTO targetUser = userService.getUserInfoById(session.getTargetUserId()); + if (targetUser != null){ + vo.setSessionName(targetUser.getNickname()); + vo.setAvatarUrl(targetUser.getAvatarUrl()); + }else { + vo.setSessionName("用户" + session.getTargetUserId()); + vo.setAvatarUrl(null); + } + return vo; + }).collect(Collectors.toList()); + } + + /** + * 更新会话状态 + * @param userId 用户ID + * @param dto 会话状态 + */ + @Override + public void updateSessionStatus(Long userId, ChatSessionStatusDTO dto) { + chatSessionsMapper.updateSessionStatus( + userId, + dto.getTargetUserId(), + dto.getSessionStatus() + ); + } + + /** + * 置顶会话 + * @param userId 用户ID + * @param dto 置顶状态 + */ + @Override + public void updateTopStatus(Long userId, ChatSessionTopDTO dto) { + chatSessionsMapper.updateTopStatus( + userId, + dto.getTargetUserId(), + dto.getTopStatus() + ); + } + + /** + * 免打扰会话 + * @param userId 用户ID + * @param dto 免打扰状态 + */ + @Override + public void updateMuteStatus(Long userId, ChatSessionMuteDTO dto) { + chatSessionsMapper.updateMuteStatus( + userId, + dto.getTargetUserId(), + 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; + } +} diff --git a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java index 3477053..4785af1 100644 --- a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java +++ b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java @@ -7,10 +7,9 @@ import com.bao.dating.common.aliyun.GreenImageScan; import com.bao.dating.common.aliyun.GreenTextScan; import com.bao.dating.common.result.AliOssResult; import com.bao.dating.common.result.GreenAuditResult; -import com.bao.dating.config.RedisConfig; import com.bao.dating.context.UserContext; import com.bao.dating.mapper.UserMapper; -import com.bao.dating.pojo.dto.UserInfoUpdateDTO; +import com.bao.dating.pojo.dto.UserInfoDTO; import com.bao.dating.pojo.dto.UserLoginDTO; import com.bao.dating.pojo.entity.User; import com.bao.dating.pojo.vo.UserInfoVO; @@ -237,7 +236,7 @@ public class UserServiceImpl implements UserService { * @param userInfoUpdateDTO 用户信息更新参数 */ @Override - public UserInfoVO updateUserInfo(UserInfoUpdateDTO userInfoUpdateDTO) { + public UserInfoVO updateUserInfo(UserInfoDTO userInfoUpdateDTO) { Long userId = userInfoUpdateDTO.getUserId(); User user = userMapper.selectByUserId(userId); if (user == null) { @@ -319,22 +318,23 @@ public class UserServiceImpl implements UserService { } /** - * 查询用户 - * @param userName 用户民称 - * @return + * 用户注册 + * @param userName 用户名称 + * @param userPassword 用户密码 + * @return 注册结果 */ @Override - public Boolean registerUser(String userName,String userPassword) { + public Boolean registerUser(String userName, String userPassword) { //校验参数是否为空 if (userName.isEmpty() || userPassword.isEmpty()){ return false; } - //产看数据库是否存在已注册用户 + //查看数据库是否存在已注册用户 User user = userMapper.getByUsername(userName); if (user != null){ return false; } - //将用户数据存入苏数据库 + //将用户数据存入数据库 String salt = "lyy123"; String passwordHash = MD5Util.encryptWithSalt(userPassword, salt); //查询最大用户id @@ -372,4 +372,23 @@ public class UserServiceImpl implements UserService { userLoginVO.setToken(token); return userLoginVO; } + + /** + * 根据用户ID获取用户昵称和头像 + * + * @param userId 用户ID + * @return 用户昵称 + */ + @Override + public UserInfoDTO getUserInfoById(Long userId) { + // 查询数据库获取昵称 + User user = userMapper.selectByUserId(userId); + if (user == null) { + throw new RuntimeException("没有此用户"); + } + UserInfoDTO dto = new UserInfoDTO(); + dto.setUserId(user.getUserId()); + dto.setNickname(user.getNickname()); + return dto; + } } diff --git a/src/main/java/com/bao/dating/session/WsSessionManager.java b/src/main/java/com/bao/dating/session/WsSessionManager.java new file mode 100644 index 0000000..90eea45 --- /dev/null +++ b/src/main/java/com/bao/dating/session/WsSessionManager.java @@ -0,0 +1,51 @@ +package com.bao.dating.session; + +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * WebSocketSession 管理类 + * @author KilLze + */ +@Component +public class WsSessionManager { + + private final Map SESSION_MAP = new ConcurrentHashMap<>(); + /** + * 添加 WebSocketSession + * @param userId 用户ID + * @param session WebSocketSession + */ + public void addSession(Long userId, WebSocketSession session) { + SESSION_MAP.put(userId, session); + } + + /** + * 移除 WebSocketSession + * @param userId 用户ID + */ + public void removeSession(Long userId) { + SESSION_MAP.remove(userId); + } + + /** + * 获取 WebSocketSession + * @param userId 用户ID + * @return WebSocketSession + */ + public WebSocketSession getSession(Long userId) { + return SESSION_MAP.get(userId); + } + + /** + * 判断用户是否在线 + * @param userId 用户ID + * @return true-在线,false-离线 + */ + public boolean isOnline(Long userId) { + return SESSION_MAP.containsKey(userId); + } +} diff --git a/src/main/resources/com/bao/dating/mapper/ChatRecordsMapper.xml b/src/main/resources/com/bao/dating/mapper/ChatRecordsMapper.xml new file mode 100644 index 0000000..5d3ddda --- /dev/null +++ b/src/main/resources/com/bao/dating/mapper/ChatRecordsMapper.xml @@ -0,0 +1,92 @@ + + + + + + + INSERT INTO chat_records + ( + sender_user_id, + receiver_user_id, + message_content, + message_type, + read_status, + send_time, + message_status, + created_at, + updated_at + ) + VALUES + ( + #{senderUserId}, + #{receiverUserId}, + #{messageContent}, + #{messageType}, + #{readStatus}, + #{sendTime}, + #{messageStatus}, + NOW(), + NOW() + ) + + + + + + + + UPDATE chat_records + SET + read_status = 1, + read_time = #{readTime}, + updated_at = NOW() + WHERE + sender_user_id = #{senderUserId} + AND receiver_user_id = #{receiverUserId} + AND read_status = 0 + AND message_status = 1 + + + + + + + + UPDATE chat_records + SET + message_status = 2, + updated_at = NOW() + WHERE + chat_id = #{chatId} + AND sender_user_id = #{senderUserId} + AND message_status = 1 + + + \ No newline at end of file diff --git a/src/main/resources/com/bao/dating/mapper/ChatSessionsMapper.xml b/src/main/resources/com/bao/dating/mapper/ChatSessionsMapper.xml new file mode 100644 index 0000000..1688893 --- /dev/null +++ b/src/main/resources/com/bao/dating/mapper/ChatSessionsMapper.xml @@ -0,0 +1,91 @@ + + + + + + + INSERT INTO chat_sessions + (user_id, target_user_id, session_name, last_message_id, last_message_content, last_message_time, + unread_count, session_status, top_status, mute_status, created_at, updated_at) + VALUES + ( + #{userId}, #{targetUserId}, #{sessionName}, #{lastMessageId}, #{lastMessageContent}, #{lastMessageTime}, + 0, 1, 0, 0, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + last_message_id = VALUES(last_message_id), + last_message_content = VALUES(last_message_content), + last_message_time = VALUES(last_message_time), + unread_count = 0, + updated_at = NOW(); + + + + + INSERT INTO chat_sessions + (user_id, target_user_id, session_name, last_message_id, last_message_content, last_message_time, + unread_count, session_status, top_status, mute_status, created_at, updated_at) + VALUES + ( + #{userId}, #{targetUserId}, #{sessionName}, #{lastMessageId}, #{lastMessageContent}, #{lastMessageTime}, + 1, 1, 0, 0, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + last_message_id = VALUES(last_message_id), + last_message_content = VALUES(last_message_content), + last_message_time = VALUES(last_message_time), + unread_count = unread_count + 1, + updated_at = NOW(); + + + + + UPDATE chat_sessions + SET + unread_count = 0, + updated_at = NOW() + WHERE + user_id = #{userId} + AND target_user_id = #{targetUserId} + AND session_status = 1 + + + + + + + + UPDATE chat_sessions + SET session_status = #{sessionStatus}, + updated_at = NOW() + WHERE user_id = #{userId} + AND target_user_id = #{targetUserId} + + + + + UPDATE chat_sessions + SET top_status = #{topStatus}, + updated_at = NOW() + WHERE user_id = #{userId} + AND target_user_id = #{targetUserId} + + + + + UPDATE chat_sessions + SET mute_status = #{muteStatus}, + updated_at = NOW() + WHERE user_id = #{userId} + AND target_user_id = #{targetUserId} + + + \ No newline at end of file