重构部分接口,改普通分页查询为游标滚动分页查询,优化缓存结构,完善消息模块

newzwz
forely 1 week ago
parent fa9627a9de
commit 70150357e6

@ -4,6 +4,7 @@ import lombok.Data;
@Data
public class PageRequest {
// 普通分页参数
private Long current = 1L;
private Long size = 10L;
}

@ -13,8 +13,9 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
private Long current;
private Long size = 10L;
// 普通分页参数
private Long current; // 当前页数,适用于普通分页
private Long total;
private Long size = 10L;
private List<T> records = Collections.emptyList();
}

@ -0,0 +1,14 @@
package com.luojia_channel.common.domain.page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScrollPageRequest {
private Long lastVal; // 上次查询的最小值(用于游标分页)
private Integer offset = 0; // 偏移量(用于分页位置标记)
private Long size = 10L; // 每页数量
}

@ -0,0 +1,20 @@
package com.luojia_channel.common.domain.page;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
// 滚动分页请求
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ScrollPageResponse<T> {
private Long lastVal; // 上次查询的最小值(用于游标分页)
private Integer offset = 0; // 偏移量(用于分页位置标记)
private Long size = 10L; // 每页数量
private List<T> records; // 数据列表
}

@ -1,5 +1,9 @@
package com.luojia_channel.common.utils;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@ -9,10 +13,12 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -42,7 +48,9 @@ public class RedisUtil {
return value != null ? type.cast(value) : null;
}
public <T> T safeGet(String key, Class<T> type, Supplier<T> cacheLoader, long timeout, TimeUnit timeUnit) {
// 安全地从缓存中取值
public <T> T safeGet(String key, Class<T> type, Supplier<T> cacheLoader,
long timeout, TimeUnit timeUnit) {
T result = get(key, type);
if(result != null){
return result;
@ -65,6 +73,42 @@ public class RedisUtil {
return get(key, type);
}
// 封装基于redis zset的滚动分页查询
public <T> ScrollPageResponse<T> scrollPageQuery(String key, Class<T> type,
ScrollPageRequest pageRequest,
Function<List<Long>, List<T>> dbFallback) {
long max = pageRequest.getLastVal();
long offset = pageRequest.getOffset();
long size = pageRequest.getSize();
Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, size);
if(typedTuples == null || typedTuples.isEmpty()){
return ScrollPageResponse.<T>builder().build();
}
// 获取返回的offset与minTime
List<Long> ids = new ArrayList<>();
int returnOffset = 1;
long min = 0;
for (ZSetOperations.TypedTuple<Object> tuple : typedTuples) {
Long id = (Long)tuple.getValue();
ids.add(id);
long lastVal = tuple.getScore().longValue();
if(lastVal == min){
returnOffset++;
}else{
returnOffset = 1;
min = lastVal;
}
}
List<T> dbList = dbFallback.apply(ids);
return ScrollPageResponse.<T>builder()
.records(dbList)
.size(pageRequest.getSize())
.offset(returnOffset)
.lastVal(min)
.build();
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT);
}
@ -200,4 +244,27 @@ public class RedisUtil {
}
public <T> List<ZSetItem<T>> zRevRangeWithScores(String key, long count) {
return zRevRangeWithScores(key, 0, count - 1);
}
public <T> List<ZSetItem<T>> zRevRangeWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
return convertTuples(tuples);
}
public <T> T zRevMaxValue(String key) {
List<ZSetItem<T>> items = zRevRangeWithScores(key, 1);
return items.isEmpty() ? null : items.get(0).getValue();
}
public <T> ZSetItem<T> zRevMaxItem(String key) {
List<ZSetItem<T>> items = zRevRangeWithScores(key, 1);
return items.isEmpty() ? null : items.get(0);
}
}

@ -1,34 +1,41 @@
package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.modules.message.dto.MessageRequest;
import com.luojia_channel.modules.message.server.WebSocketServer;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.interact.service.ChatService;
import com.luojia_channel.modules.message.dto.MessageResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/message")
@RequiredArgsConstructor
@Tag(name = "聊天模块", description = "好友聊天模块相关接口")
public class ChatController {
private final ChatService chatService;
private WebSocketServer webSocketServer;
@Operation(
summary = "聊天列表",
description = "传入分页参数,查询私信用户列表(带最新消息)",
tags = {"私信模块"}
)
@GetMapping("/chat-list")
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatList(chatPageQueryDTO));
}
@PostMapping("/sendPrivateMessage")
@Operation(
summary = "发送私信",
description = "发送私信给指定用户",
tags = {"聊天模块"}
summary = "历史记录",
description = "传入分页参数,获取与特定用户的完整聊天记录",
tags = {"关注模块"}
)
public String sendPrivateMessage(@RequestParam Long senderId, @RequestBody MessageRequest request) {
try {
webSocketServer.sendPrivateMessage(senderId, request);
return "私信发送成功";
} catch (Exception e) {
return "私信发送失败: " + e.getMessage();
}
@GetMapping("/history")
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatHistory(chatPageQueryDTO));
}
}

@ -2,7 +2,11 @@ package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -39,11 +43,21 @@ public class FollowController {
}
@GetMapping("/common/{id}")
@Operation(
summary = "关注列表",
description = "传入用户id返回该用户的关注列表",
summary = "共同关注",
description = "传入用户id返回该与该用户的共同关注",
tags = {"关注模块"}
)
public Result<List<UserDTO>> followCommons(@PathVariable("id") Long id){
return Result.success(followService.followCommons(id));
}
@GetMapping("/post")
@Operation(
summary = "关注收件箱",
description = "传入分页参数,查询关注的人的发帖推送",
tags = {"关注模块"}
)
public Result<ScrollPageResponse<PostBasicInfoDTO>> queryPostFollow(@RequestBody PostPageQueryDTO postPageQueryDTO){
return Result.success(followService.queryPostFollow(postPageQueryDTO));
}
}

@ -0,0 +1,51 @@
package com.luojia_channel.modules.interact.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "聊天列表项DTO")
public class ChatItemDTO {
@Schema(
description = "聊天对象的用户ID",
required = true,
example = "123456"
)
private Long chatUserId;
@Schema(
description = "聊天对象的头像URL",
example = "https://example.com/avatar.jpg"
)
private String avatar;
@Schema(
description = "聊天对象的用户名",
required = true,
example = "张三"
)
private String username;
@Schema(
description = "最新消息内容",
required = true,
maxLength = 500,
example = "今天下午开会"
)
private String latestMessage;
@Schema(
description = "最新消息时间",
required = true,
example = "2023-10-15T14:30:00"
)
private LocalDateTime latestTime;
}

@ -0,0 +1,9 @@
package com.luojia_channel.modules.interact.dto;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import lombok.Data;
@Data
public class ChatPageQueryDTO extends ScrollPageRequest {
private Long chatUserId;
}

@ -0,0 +1,17 @@
package com.luojia_channel.modules.interact.service;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import java.util.List;
public interface ChatService {
ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO);
ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO);
}

@ -2,7 +2,11 @@ package com.luojia_channel.modules.interact.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import java.util.List;
@ -14,4 +18,6 @@ public interface FollowService extends IService<Follow> {
boolean isFollow(Long followUserId);
List<UserDTO> followCommons(Long id);
ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO);
}

@ -0,0 +1,128 @@
package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.interact.service.ChatService;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import com.luojia_channel.modules.message.mapper.MessageMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final MessageMapper messageMapper;
private final UserMapper userMapper;
private final RedisUtil redisUtil;
@Override
public ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO) {
/*
Long userId = UserContext.getUserId();
IPage<ChatItemDTO> chatPage = messageMapper.selectChatList(PageUtil.convert(chatPageQueryDTO), userId);
return PageResponse.<ChatItemDTO>builder()
.current(chatPage.getCurrent())
.size(chatPage.getSize())
.total(chatPage.getTotal())
.records(chatPage.getRecords())
.build();
*/
Long userId = UserContext.getUserId();
String key = "chat:user_list:" + userId;
return redisUtil.scrollPageQuery(key, ChatItemDTO.class, chatPageQueryDTO,
(chatUserIds) -> {
List<ChatItemDTO> chatItems = new ArrayList<>();
List<Long> latestMessageIds = new ArrayList<>();
List<User> users = userMapper.selectByIdsOrderByField(chatUserIds);
for(Long chatUserId : chatUserIds){
String messageKey = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
// 获取zset中最新的messageId
Long latestMessageId = redisUtil.zRevMaxValue(messageKey);
latestMessageIds.add(latestMessageId);
}
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(latestMessageIds);
int i=0;
for(User user : users){
ChatItemDTO chatItemDTO = ChatItemDTO.builder()
.chatUserId(user.getId())
.avatar(user.getAvatar())
.username(user.getUsername())
.latestMessage(messageDOS.get(i).getContent())
.latestTime(messageDOS.get(i).getCreateTime())
.build();
chatItems.add(chatItemDTO);
i++;
}
return chatItems;
});
}
@Override
public ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO) {
/*
Long userId = UserContext.getUserId();
Long chatUserId = chatPageQueryDTO.getChatUserId();
LambdaQueryWrapper<MessageDO> queryWrapper = Wrappers.lambdaQuery(MessageDO.class)
.eq(MessageDO::getSenderId, userId)
.eq(MessageDO::getReceiverId, chatUserId)
.or()
.eq(MessageDO::getReceiverId, userId)
.eq(MessageDO::getSenderId, chatUserId)
.orderByDesc(MessageDO::getCreateTime);
// 查询的是私信消息
queryWrapper.eq(MessageDO::getMessageType, 1);
IPage<MessageDO> page = messageMapper.selectPage(PageUtil.convert(chatPageQueryDTO), queryWrapper);
User chatUser = userMapper.selectById(chatUserId);
return PageUtil.convert(page, (message) -> {
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
if(messageResponse.getSenderId().equals(userId)) {
messageResponse.setSenderAvatar(UserContext.getAvatar());
messageResponse.setSenderName(UserContext.getUsername());
}else{
messageResponse.setSenderAvatar(chatUser.getAvatar());
messageResponse.setSenderName(chatUser.getUsername());
}
return messageResponse;
});
*/
// 改成滚动分页查询
Long userId = UserContext.getUserId();
Long chatUserId = chatPageQueryDTO.getChatUserId();
String key = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
return redisUtil.scrollPageQuery(key, MessageResponse.class, chatPageQueryDTO,
(messageIds) -> {
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(messageIds);
User chatUser = userMapper.selectById(chatUserId);
List<MessageResponse> messageResponses = new ArrayList<>();
for(MessageDO message : messageDOS){
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
if(messageResponse.getSenderId().equals(userId)) {
messageResponse.setSenderAvatar(UserContext.getAvatar());
messageResponse.setSenderName(UserContext.getUsername());
}else{
messageResponse.setSenderAvatar(chatUser.getAvatar());
messageResponse.setSenderName(chatUser.getUsername());
}
messageResponses.add(messageResponse);
}
return messageResponses;
});
}
}

@ -3,19 +3,25 @@ package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@ -25,6 +31,9 @@ public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> impleme
private final FollowMapper followMapper;
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
@Override
public void follow(Long followUserId, Boolean isFollow) {
Long userId = UserContext.getUserId();
@ -70,4 +79,29 @@ public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> impleme
return userDTOS;
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
String key = "post:follow_of:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Post> posts = postMapper.selectBatchIds(postIds);
List<Long> userIds = posts.stream().map(Post::getUserId).toList();
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
User user = userMap.getOrDefault(post.getUserId(), new User());
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTOS.add(postBasicInfoDTO);
}
// 按照发布时间倒序排序
postBasicInfoDTOS.sort(Comparator.comparing(PostBasicInfoDTO::getCreateTime).reversed());
return postBasicInfoDTOS;
});
}
}

@ -1,10 +1,21 @@
package com.luojia_channel.modules.message.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.message.entity.MessageDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MessageMapper extends BaseMapper<MessageDO> {
/**
*
*/
IPage<ChatItemDTO> selectChatList(IPage<ChatItemDTO> page, @Param("userId") Long userId);
List<MessageDO> selectByIdsOrderByField(@Param("ids") List<Long> ids);
}

@ -33,7 +33,7 @@ public class NotificationListener {
NotificationMessage message = wrapper.getMessage();
MessageRequest request = BeanUtil.copyProperties(message, MessageRequest.class);
Integer messageType = message.getMessageType();
if (messageType != null && messageType == 0) {
if (messageType != null && !messageType.equals(0)) {
webSocketServer.sendPrivateMessage(message.getSenderId(), request);
} else {
webSocketServer.sendSystemNotification(request);

@ -2,14 +2,15 @@ package com.luojia_channel.modules.message.server;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.message.dto.MessageRequest;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import com.luojia_channel.modules.message.mapper.MessageMapper;
import com.luojia_channel.modules.message.util.WebSocketContext;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -21,11 +22,9 @@ import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/connect/{userId}")
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketServer {
// 存储在线用户会话 <userId, Session>
private final static Map<String, Session> CLIENTS = new ConcurrentHashMap<>();
private final MessageMapper messageMapper;
@OnOpen
public void onOpen(@PathParam("userId") String userId,
@ -60,16 +59,10 @@ public class WebSocketServer {
try {
// 解析客户端发送的 JSON 消息
MessageRequest request = JSON.parseObject(message, MessageRequest.class);
switch (request.getMessageType()) {
case 0:
sendPrivateMessage(Long.parseLong(senderId), request);
break;
case 1:
sendSystemNotification(request);
break;
default:
log.warn("未知消息类型: {}", request.getMessageType());
case 0 -> sendSystemNotification(request);
case 1 -> sendPrivateMessage(Long.parseLong(senderId), request);
default -> log.warn("未知消息类型: {}", request.getMessageType());
}
} catch (Exception e) {
log.error("消息处理失败: {}", e.getMessage());
@ -83,7 +76,7 @@ public class WebSocketServer {
Session receiverSession = CLIENTS.get(receiverId.toString());
// 构建私信响应
MessageResponse response = MessageResponse.builder()
.messageType(0)
.messageType(request.getMessageType())
.content(request.getContent())
.senderId(senderId)
.senderName(request.getSenderName())
@ -99,14 +92,27 @@ public class WebSocketServer {
log.info("接收方 [{}] 不在线,消息无法即时送达", receiverId);
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
RedisUtil redisUtil = WebSocketContext.getBean(RedisUtil.class);
messageMapper.insert(message);
sendMessage(CLIENTS.get(senderId.toString()), JSON.toJSONString(response));
// 存储消息至redis
if(request.getMessageType().equals(1)){
String key = "chat:history:" + Math.min(senderId, receiverId) + ":" +Math.max(senderId, receiverId);
redisUtil.zAdd(key, message.getId(), System.currentTimeMillis());
String chatListKey = "chat:user_list:" + senderId;
redisUtil.zAdd(chatListKey, receiverId, System.currentTimeMillis());
chatListKey = "chat:user_list:" + receiverId;
redisUtil.zAdd(chatListKey, senderId, System.currentTimeMillis());
}
}
// 发送系统通知
public void sendSystemNotification(MessageRequest request) {
MessageResponse response = MessageResponse.builder()
.messageType(1)
.senderId(0L)
.receiverId(0L)
.messageType(request.getMessageType())
.content(request.getContent())
.createTime(LocalDateTime.now())
.build();
@ -115,6 +121,7 @@ public class WebSocketServer {
sendMessage(session, JSON.toJSONString(response));
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
messageMapper.insert(message);
}

@ -0,0 +1,43 @@
package com.luojia_channel.modules.message.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class WebSocketContext implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
@Autowired
public void setApplicationContext(ApplicationContext inApplicationContext) throws BeansException {
applicationContext = inApplicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
public static String getActiveProfile() {
String[] activeProfiles = getApplicationContext().getEnvironment().getActiveProfiles();
if (activeProfiles.length == 0) {
return null;
}
return activeProfiles[0];
}
}

@ -2,6 +2,7 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
@ -37,9 +38,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Void> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
commentService.saveComment(commentSaveDTO);
return Result.success();
public Result<Long> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
return Result.success(commentService.saveComment(commentSaveDTO));
}
// 更新评论
@ -82,8 +82,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法")
})
public Result<PageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
PageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
return Result.success(commentList);
}
@ -98,8 +98,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败评论ID不合法")
})
public Result<PageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
PageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
return Result.success(commentInfoDTOList);
}

@ -3,6 +3,7 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -34,9 +35,8 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Void> savePost(@RequestBody PostSaveDTO postSaveDTO) {
postService.savePost(postSaveDTO);
return Result.success();
public Result<Long> savePost(@RequestBody PostSaveDTO postSaveDTO) {
return Result.success(postService.savePost(postSaveDTO));
}
// 设置帖子封面
@ -107,22 +107,22 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<PageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePost(postPageQueryDTO));
}
// 查看自己的帖子
@GetMapping("/of/me")
// 查看用户的帖子
@GetMapping("/user")
@Operation(
summary = "查看自己的帖子",
summary = "查看用户的帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<PageResponse<PostBasicInfoDTO>> pagePostOfMe(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfMe(postPageQueryDTO));
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfUser(postPageQueryDTO));
}
// 点赞帖子

@ -1,15 +1,20 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(title = "分页查询评论请求DTO")
public class CommentPageQueryDTO extends PageRequest {
public class CommentPageQueryDTO extends ScrollPageRequest {
@Schema(title = "帖子ID")
private Long postId;
@Schema(title = "评论ID")
private Long commentId;
private Long parentCommentId;
private Boolean orderByTime = true;
private Boolean orderByHot = false;
}

@ -34,12 +34,8 @@ public class CommentSaveDTO {
private Long postId;
@Schema(
description = "该评论的父评论id"
description = "该评论的父评论id,若不是回复则传入空值"
)
private Long parentCommentId;
@Schema(
description = "该评论的顶级评论id"
)
private Long topId;
}

@ -1,9 +1,14 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PostPageQueryDTO extends PageRequest {
public class PostPageQueryDTO extends ScrollPageRequest {
@Schema(
description = "想要查看的用户的id输入空时为自己的id"
)
private Long userId;
}

@ -3,6 +3,8 @@ package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "帖子基本信息")
public class PostBasicInfoDTO {
@ -62,4 +64,8 @@ public class PostBasicInfoDTO {
description = "匿名情况下用户头像"
)
private String userAvatar;
@Schema(
description = "帖子创建时间"
)
private LocalDateTime createTime;
}

@ -3,6 +3,8 @@ package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "修改帖子信息")
public class PostInfoDTO {
@ -72,4 +74,8 @@ public class PostInfoDTO {
description = "匿名情况下用户头像"
)
private String userAvatar;
@Schema(
description = "帖子创建时间"
)
private LocalDateTime createTime;
}

@ -2,7 +2,11 @@ package com.luojia_channel.modules.post.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
List<Comment> selectByIdsOrderByField(@Param("ids") List<Long> ids);
}

@ -4,7 +4,11 @@ package com.luojia_channel.modules.post.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Post;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PostMapper extends BaseMapper<Post> {
List<Post> selectByIdsOrderByField(@Param("ids")List<Long> ids);
}

@ -1,6 +1,7 @@
package com.luojia_channel.modules.post.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
@ -12,15 +13,15 @@ import java.util.List;
@Service
public interface CommentService {
void saveComment(CommentSaveDTO commentSaveDTO);
Long saveComment(CommentSaveDTO commentSaveDTO);
void updateComment(CommentSaveDTO commentSaveDTO);
void deleteComment(Long id);
PageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO);
ScrollPageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO);
PageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO);
ScrollPageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO);
void likeComment(Long id);
}

@ -2,6 +2,7 @@ package com.luojia_channel.modules.post.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -10,7 +11,7 @@ import com.luojia_channel.modules.post.entity.Post;
import org.springframework.web.multipart.MultipartFile;
public interface PostService extends IService<Post> {
void savePost(PostSaveDTO postSaveDTO);
Long savePost(PostSaveDTO postSaveDTO);
String setCover(MultipartFile file);
@ -20,9 +21,9 @@ public interface PostService extends IService<Post> {
PostInfoDTO getPostDetail(Long id);
PageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO);
ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO);
PageResponse<PostBasicInfoDTO> pagePostOfMe(PostPageQueryDTO postPageQueryDTO);
ScrollPageResponse<PostBasicInfoDTO> pagePostOfUser(PostPageQueryDTO postPageQueryDTO);
void likePost(Long id);
}

@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.exception.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil;
@ -54,75 +55,102 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
@Override
@Transactional(rollbackFor = Exception.class)
public void saveComment(CommentSaveDTO commentSaveDTO) {
public Long saveComment(CommentSaveDTO commentSaveDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
if (userId == null) {
throw new UserException("用户未登录");
}
validatePostUtil.validateComment(commentSaveDTO);
Comment comment = BeanUtil.copyProperties(commentSaveDTO, Comment.class);
comment.setUserId(UserContext.getUserId());
comment.setUserId(userId);
comment.setCreateTime(LocalDateTime.now());
comment.setUpdateTime(LocalDateTime.now());
if(!save(comment)){
if (!save(comment)) {
throw new PostException("创建评论失败");
}
Long postId = commentSaveDTO.getPostId();
// 保存成功后,若为根级评论,设置 topId 为评论 id
if (comment.getParentCommentId() == null) {
comment.setTopId(comment.getId());
comment.setParentCommentId(0L);
if (!updateById(comment)) {
throw new PostException("更新根级评论 topId 失败");
}
// 同时更新帖子的zset列表
String key = "post:comment_by_time:" + comment.getPostId();
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
}
// 帖子的回复数加一
Long postId = comment.getPostId();
Post post = postMapper.selectById(postId);
if (post == null) {
throw new PostException("回复的帖子不存在");
}
post.setCommentCount(post.getCommentCount() + 1);
if(postMapper.updateById(post) <= 0){
throw new PostException("回复帖子失败");
}
Long receiveUserId = post.getUserId();
Long parentCommentId = commentSaveDTO.getParentCommentId();
Long parentCommentId = comment.getParentCommentId();
// 消息通知,回复帖子
if(!userId.equals(receiveUserId) && parentCommentId == null) {
if (!userId.equals(receiveUserId) && parentCommentId == 0) {
String content = String.format("%s 回复了你的帖子: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(UserContext.getUserId())
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(receiveUserId)
.content(content)
.messageType(0)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
}
if(parentCommentId != null){
if (parentCommentId != 0) {
// 是回复的评论
Comment partentComment = commentMapper.selectById(parentCommentId);
partentComment.setReplyCount(partentComment.getReplyCount() + 1);
int update = commentMapper.updateById(partentComment);
if(update <= 0) {
throw new PostException("回复评论失败");
Comment parentComment = commentMapper.selectById(parentCommentId);
if (parentComment == null) {
throw new PostException("父评论不存在");
}
Long topId = commentSaveDTO.getTopId();
// 更新顶级评论回复数
if(!parentCommentId.equals(topId)){
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.eq(Comment::getId, topId)
.setSql("reply_count = reply_count + 1");
update = commentMapper.update(null, updateWrapper);
if(update <= 0) {
throw new PostException("回复顶级评论失败");
}
// 设置顶级评论id
comment.setTopId(parentComment.getTopId());
updateById(comment);
// 更新评论的zset回复列表
String buildKey = String.format("%d_%d", comment.getPostId(), comment.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
// 更新顶级评论回复数,当顶级评论与回复评论不同时
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.eq(Comment::getId, comment.getTopId())
.setSql("reply_count = reply_count + 1");
int update = commentMapper.update(null, updateWrapper);
if (update <= 0) {
throw new PostException("回复顶级评论失败");
}
// 消息通知,回复评论
String content = String.format("%s 回复了你的评论: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(UserContext.getUserId())
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(partentComment.getUserId())
.receiverId(parentComment.getUserId())
.content(content)
.messageType(0)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
}
return comment.getId();
}
@Override
@ -138,6 +166,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteComment(Long id) {
validatePostUtil.validateCommentOwnership(id);
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
@ -146,31 +175,83 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if(delete <= 0) {
throw new PostException("删除评论失败");
}
Comment comment = commentMapper.selectById(id);
if(comment.getId().equals(comment.getTopId())) {
redisUtil.zRemove("post:comment_by_time:" + comment.getPostId(), comment.getId());
redisUtil.delete("comment:reply_by_time:" + comment.getTopId());
}else{
redisUtil.zRemove("comment:reply_by_time:" + comment.getTopId(), comment.getId());
}
// TODO 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论
}
// 分页查询一系列根评论
@Override
public PageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
public ScrollPageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getPostId() == null || commentPageQueryDTO.getPostId() < 0){
throw new PostException("帖子id不合法");
}
/*
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getPostId, commentPageQueryDTO.getPostId())
.eq(Comment::getParentCommentId, 0L)
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String key = "post:comment_by_time:" + commentPageQueryDTO.getPostId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<CommentInfoDTO> commentInfoDTOS = new ArrayList<>();
for(Comment comment : comments){
CommentInfoDTO commentInfoDTO = BeanUtil.copyProperties(comment, CommentInfoDTO.class);
User user = userMap.getOrDefault(comment.getUserId(), new User());
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
commentInfoDTOS.add(commentInfoDTO);
}
return commentInfoDTOS;
});
}
@Override
public PageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getCommentId() == null || commentPageQueryDTO.getCommentId() < 0){
public ScrollPageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getParentCommentId() == null || commentPageQueryDTO.getParentCommentId() < 0){
throw new PostException("评论id不合法");
}
/*
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getTopId, commentPageQueryDTO.getCommentId())
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String buildKey = String.format("%d_%d", commentPageQueryDTO.getPostId(), commentPageQueryDTO.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<CommentInfoDTO> commentInfoDTOS = new ArrayList<>();
for(Comment comment : comments){
CommentInfoDTO commentInfoDTO = BeanUtil.copyProperties(comment, CommentInfoDTO.class);
User user = userMap.getOrDefault(comment.getUserId(), new User());
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
commentInfoDTOS.add(commentInfoDTO);
}
return commentInfoDTOS;
});
}
@Override
@ -208,6 +289,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return commentMapper.update(null, updateWrapper) > 0;
}
/*
private PageResponse<CommentInfoDTO> getCommentInfoDTOPageResponse(CommentPageQueryDTO commentPageQueryDTO, LambdaQueryWrapper<Comment> queryWrapper) {
IPage<Comment> commentPage = commentMapper.selectPage(PageUtil.convert(commentPageQueryDTO), queryWrapper);
List<Long> userIds = new ArrayList<>();
@ -224,6 +306,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return commentInfoDTO;
});
}
*/
private List<CommentInfoDTO> convertToDTO(List<Comment> commentList) {
List<CommentInfoDTO> dtos = new ArrayList<>();

@ -6,12 +6,17 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.exception.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.service.impl.FileServiceImpl;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.impl.FollowServiceImpl;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -23,8 +28,10 @@ import com.luojia_channel.modules.post.utils.ValidatePostUtil;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.ArrayList;
@ -46,9 +53,12 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// 匿名用户名与匿名头像
private static final String ANONYMOUS_NAME = "匿名用户";
private static final String ANONYMOUS_AVATAR = "";
private final FollowMapper followMapper;
private final FollowServiceImpl followService;
@Override
public void savePost(PostSaveDTO postSaveDTO) {
@Transactional(rollbackFor = Exception.class)
public Long savePost(PostSaveDTO postSaveDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
@ -62,7 +72,21 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
throw new PostException("创建帖子失败");
}
redisUtil.delete("post:detail:" + postSaveDTO.getId());
redisUtil.delete("post:of:user:" + UserContext.getUserId());
//redisUtil.delete("post:of:user:" + UserContext.getUserId());
// 非匿名帖子推送给粉丝
if(post.getStatus().equals(0)){
List<Follow> follows = followMapper.selectList(Wrappers.lambdaQuery(Follow.class)
.eq(Follow::getFollowUserId, userId));
for(Follow follow : follows){
Long fansId = follow.getUserId();
String key = "post:follow_of:" + fansId;
redisUtil.zAdd(key, post.getId(), System.currentTimeMillis());
}
// TODO 消息通知?
}
redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis());
return post.getId();
}
@Override
@ -80,23 +104,31 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
if(!updateById(post)){
throw new PostException("更新帖子失败");
}
redisUtil.delete("post:detail:" + postSaveDTO.getId());
redisUtil.delete("post:of:user:" + UserContext.getUserId());
// redisUtil.delete("post:detail:" + postSaveDTO.getId());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
}
@Override
public void deletePost(Long id) {
validatePostUtil.validatePostOwnership(id);
Long userId = UserContext.getUserId();
int delete = postMapper.deleteById(id);
if(delete <= 0){
throw new PostException("删除帖子失败");
}
redisUtil.delete("post:detail:" + id.toString());
redisUtil.delete("post:of:user:" + UserContext.getUserId());
// redisUtil.delete("post:detail:" + id.toString());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
redisUtil.delete("post:detail:" + id);
redisUtil.zRemove("post:time:", id);
redisUtil.zRemove("post:user:" + userId, id);
}
@Override
public PostInfoDTO getPostDetail(Long id) {
// TODO
Post oldPost = getById(id);
oldPost.setViewCount(oldPost.getViewCount() + 1);
updateById(oldPost);
return redisUtil.safeGet("post:detail:" + id, PostInfoDTO.class,
() -> {
Post post = getById(id);
@ -119,8 +151,9 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
}
@Override
public PageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
// TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则
/*
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
@ -141,27 +174,58 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
}
return postBasicInfoDTO;
});
*/
String key = "post:time:";
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Long> userIds = new ArrayList<>();
List<Post> posts = postMapper.selectByIdsOrderByField(postIds);
posts.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
// 组装用户头像与名称,批量查询只要查一次数据库
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
posts.forEach(post -> {
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
User user = userMap.getOrDefault(post.getUserId(), new User());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTO.setUserName(user.getUsername());
if (post.getStatus() == 1) { // 匿名帖子
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
postBasicInfoDTOS.add(postBasicInfoDTO);
});
return postBasicInfoDTOS;
});
}
@Override
public PageResponse<PostBasicInfoDTO> pagePostOfMe(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
public ScrollPageResponse<PostBasicInfoDTO> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) {
Long myUserId = UserContext.getUserId();
if(myUserId == null){
throw new UserException("用户未登录");
}
Long userId = postPageQueryDTO.getUserId()==null ? myUserId : postPageQueryDTO.getUserId();
/*
// 构建包含分页信息的缓存键
String cacheKey = "post:of:user:" + userId + ":page:" + postPageQueryDTO.getCurrent() + ":size:" + postPageQueryDTO.getSize();
return redisUtil.safeGet(cacheKey, PageResponse.class,
() -> {
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.eq(Post::getUserId, userId)
.orderByDesc(Post::getCreateTime);
.eq(Post::getUserId, userId);
// 当查询别人时,不显示匿名帖子
if(!myUserId.equals(userId)){
queryWrapper.eq(Post::getStatus, 0);
}
queryWrapper.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
return PageUtil.convert(postPage, post -> {
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserAvatar(UserContext.getAvatar());
postBasicInfoDTO.setUserName(UserContext.getUsername());
if (post.getStatus() == 1) { // 匿名帖子
if (post.getStatus() == 1) { // 自己的匿名帖子
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
@ -169,6 +233,34 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
});
},
60, TimeUnit.MINUTES);
*/
String key = "post:user:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Long> userIds = new ArrayList<>();
List<Post> posts = postMapper.selectByIdsOrderByField(postIds);
posts.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
// 组装用户头像与名称,批量查询只要查一次数据库
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
User user = userMap.getOrDefault(post.getUserId(), new User());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTO.setUserName(user.getUsername());
if (post.getStatus() == 1) { // 自己的匿名帖子
if(!userId.equals(user.getId())){
continue;
}
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
postBasicInfoDTOS.add(postBasicInfoDTO);
}
return postBasicInfoDTOS;
});
}
private boolean isLikedPost(Long postId){

@ -65,9 +65,6 @@ public class ValidatePostUtil {
if(commentSaveDTO.getParentCommentId() != null && commentSaveDTO.getParentCommentId() < 0){
throw new PostException("父评论id不合法");
}
if(commentSaveDTO.getTopId() != null && commentSaveDTO.getTopId() <= 0){
throw new PostException("顶级评论id不合法");
}
}
public void validateCommentOwnership(Long id) {

@ -4,7 +4,10 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<User> selectByIdsOrderByField(List<Long> ids);
}

@ -1,36 +1,36 @@
#本地开发环境
lj:
db:
host: localhost
password: 123456
redis:
host: localhost
port: 6379
password: 123456
rabbitmq:
host: localhost
port: 15672
username: root
password: 123456
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
minio:
endpoint: http://192.168.59.129:9000
accessKey: forely
secretKey: Forely123!

@ -1,22 +0,0 @@
INSERT INTO `post` (`title`, `image`, `content`, `status`, `like_count`, `comment_count`, `favorite_count`, `view_count`, `user_id`, `category_id`)
VALUES
('秋日散步记', 'http://example.com/images/post1.jpg', '今天去公园散步,看到了很多美丽的景色...', 0, 15, 8, 5, 100, 1, 1),
('美食推荐', 'http://example.com/images/post2.jpg', '这家餐厅的披萨非常好吃,强烈推荐给大家...', 1, 20, 12, 7, 150, 2, 2),
('旅行计划', 'http://example.com/images/post3.jpg', '计划下个月去云南旅游,期待已久的旅程...', 0, 10, 5, 3, 80, 3, 3),
('学习心得', 'http://example.com/images/post4.jpg', '最近学到了一个新的编程技巧,感觉很有用...', 0, 25, 18, 10, 200, 4, 4),
('电影分享', 'http://example.com/images/post5.jpg', '昨晚看了《无间道》,剧情非常精彩...', 1, 30, 20, 12, 250, 5, 5),
('健身日常', 'http://example.com/images/post6.jpg', '每天坚持锻炼身体,保持健康的生活方式...', 0, 5, 2, 1, 30, 6, 1),
('宠物趣事', 'http://example.com/images/post7.jpg', '我家的小狗今天做了件搞笑的事情...', 0, 8, 4, 2, 40, 7, 2),
('读书笔记', 'http://example.com/images/post8.jpg', '最近读了一本书,《活着》让人深思...', 1, 12, 6, 4, 60, 8, 3),
('科技前沿', 'http://example.com/images/post9.jpg', '最新的AI技术真的太神奇了改变了我们的生活...', 0, 18, 9, 6, 90, 9, 4),
('摄影技巧', 'http://example.com/images/post10.jpg', '分享几个摄影小技巧,让你的照片更加出色...', 0, 22, 11, 8, 110, 10, 5),
('音乐分享', 'http://example.com/images/post11.jpg', '最近发现了一首好听的歌,一起来听听吧...', 1, 7, 3, 2, 50, 1, 1),
('烹饪食谱', 'http://example.com/images/post12.jpg', '教大家做一道简单的家常菜...', 0, 14, 7, 5, 70, 2, 2),
('游戏体验', 'http://example.com/images/post13.jpg', '玩了一个新出的游戏,超级好玩...', 0, 21, 10, 7, 120, 3, 3),
('时尚搭配', 'http://example.com/images/post14.jpg', '分享我的最新穿搭,希望对你有所帮助...', 1, 9, 4, 3, 45, 4, 4),
('户外运动', 'http://example.com/images/post15.jpg', '周末和朋友们一起去徒步,真的很放松...', 0, 17, 8, 6, 85, 5, 5),
('艺术欣赏', 'http://example.com/images/post16.jpg', '欣赏了一些印象派画作,感受到了不一样的美...', 0, 11, 5, 2, 60, 6, 1),
('创业故事', 'http://example.com/images/post17.jpg', '一位朋友刚刚开始了他的创业之旅,很激动人心...', 1, 13, 6, 4, 75, 7, 2),
('健康饮食', 'http://example.com/images/post18.jpg', '分享一些健康的饮食习惯,让我们一起变得更健康...', 0, 19, 9, 7, 100, 8, 3),
('科技创新', 'http://example.com/images/post19.jpg', '最新的科技成果真是令人惊叹,未来可期...', 0, 24, 12, 8, 130, 9, 4),
('心灵感悟', 'http://example.com/images/post20.jpg', '写下了自己的内心感受,希望能够激励更多的人...', 1, 6, 3, 2, 55, 10, 5);

@ -157,7 +157,7 @@ CREATE TABLE `view_record` (
DROP TABLE IF EXISTS `message`;
CREATE TABLE message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_type TINYINT NOT NULL COMMENT '0-私聊, 1-系统消息',
message_type TINYINT NOT NULL COMMENT '0系统消息1私信消息2评论通知',
content TEXT NOT NULL,
sender_id BIGINT NOT NULL,
receiver_id BIGINT NOT NULL,

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.message.mapper.MessageMapper">
<!-- 定义 ChatItemDTO 结果映射 -->
<resultMap id="ChatItemDTOResultMap" type="com.luojia_channel.modules.interact.dto.ChatItemDTO">
<id property="chatUserId" column="chat_user_id"/>
<result property="avatar" column="avatar"/>
<result property="username" column="username"/>
<result property="latestMessage" column="latest_message"/>
<result property="latestTime" column="create_time"/>
</resultMap>
<!-- 查询用户的所有聊天对象及最新消息 -->
<select id="selectChatList" resultMap="ChatItemDTOResultMap">
SELECT
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END AS chat_user_id,
u.avatar,
u.username,
m.content AS latest_message,
m.create_time
FROM message m
JOIN (
-- 子查询获取每个聊天对象的最新消息ID
SELECT
MAX(id) AS max_id
FROM message
WHERE sender_id = #{userId} OR receiver_id = #{userId}
GROUP BY
CASE
WHEN sender_id = #{userId} THEN receiver_id
ELSE sender_id
END
) latest ON m.id = latest.max_id
LEFT JOIN user u ON
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END = u.id
WHERE
m.sender_id = #{userId} OR m.receiver_id = #{userId}
GROUP BY
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END
ORDER BY
m.create_time DESC
</select>
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.message.entity.MessageDO">
SELECT * FROM message
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.CommentMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Comment">
SELECT * FROM comment
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.PostMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Post">
SELECT * FROM post
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -2,5 +2,11 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.user.mapper.UserMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.user.entity.User">
SELECT * FROM user
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -1,36 +1,36 @@
#本地开发环境
lj:
db:
host: localhost
password: 123456
redis:
host: localhost
port: 6379
password: 123456
rabbitmq:
host: localhost
port: 15672
username: root
password: 123456
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
minio:
endpoint: http://192.168.59.129:9000
accessKey: forely
secretKey: Forely123!

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save