feat(user): 新增用户关注列表查询功能
- 新增查询用户关注列表接口,支持分页查询 - 新增批量查询用户信息接口,提升查询效率 - 优化 MQ 消费模式为顺序消费,确保关注/取关操作有序性 - 完善用户信息 DTO,新增简介字段 - 新增分页响应封装类,支持分页查询结果返回 - 优化 Redis 查询逻辑,支持从缓存中分页获取关注列表 - 新增 Lua 脚本结果类型设置,确保脚本执行结果正确解析 - 添加 HTTP 接口测试用例,覆盖关注列表及批量查询接口 - 实现缓存与数据库双写一致性,提高数据查询性能
This commit is contained in:
@@ -16,6 +16,7 @@ import jakarta.annotation.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.common.message.Message;
|
||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -32,7 +33,8 @@ import java.util.Objects;
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
consumerGroup = "han_note_group_" + MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW,
|
||||
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW
|
||||
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW,
|
||||
consumeMode = ConsumeMode.ORDERLY
|
||||
)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -165,6 +167,7 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||
// Lua脚本
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_check_and_update_fans_zset.lua")));
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 时间戳
|
||||
long timestamp = DateUtils.localDateTime2Timestamp(createTime);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.controller;
|
||||
|
||||
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||
import com.hanserwei.framework.common.response.PageResponse;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.RelationService;
|
||||
@@ -32,4 +35,10 @@ public class RelationController {
|
||||
public Response<?> unfollow(@Validated @RequestBody UnfollowUserReqVO unfollowUserReqVO) {
|
||||
return relationService.unfollow(unfollowUserReqVO);
|
||||
}
|
||||
|
||||
@PostMapping("/following/list")
|
||||
@ApiOperationLog(description = "查询用户关注列表")
|
||||
public PageResponse<FindFollowingUserRspVO> findFollowingList(@Validated @RequestBody FindFollowingListReqVO findFollowingListReqVO) {
|
||||
return relationService.findFollowingList(findFollowingListReqVO);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.rpc;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.api.UserFeignApi;
|
||||
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
|
||||
import com.hanserwei.hannote.user.dto.req.FindUsersByIdsReqDTO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
@@ -34,4 +37,22 @@ public class UserRpcService {
|
||||
return response.getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询用户信息
|
||||
*
|
||||
* @param userIds 用户 ID集合
|
||||
* @return 用户信息集合
|
||||
*/
|
||||
public List<FindUserByIdRspDTO> findByIds(List<Long> userIds) {
|
||||
FindUsersByIdsReqDTO findUsersByIdsReqDTO = new FindUsersByIdsReqDTO();
|
||||
findUsersByIdsReqDTO.setIds(userIds);
|
||||
|
||||
Response<List<FindUserByIdRspDTO>> response = userFeignApi.findByIds(findUsersByIdsReqDTO);
|
||||
|
||||
if (!response.isSuccess() || Objects.isNull(response.getData()) || CollUtil.isEmpty(response.getData())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.getData();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.service;
|
||||
|
||||
import com.hanserwei.framework.common.response.PageResponse;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
|
||||
|
||||
@@ -20,4 +23,11 @@ public interface RelationService {
|
||||
* @return 响应
|
||||
*/
|
||||
Response<?> unfollow(UnfollowUserReqVO unfollowUserReqVO);
|
||||
|
||||
/**
|
||||
* 查询关注列表
|
||||
* @param findFollowingListReqVO 查询关注列表请求
|
||||
* @return 响应
|
||||
*/
|
||||
PageResponse<FindFollowingUserRspVO> findFollowingList(FindFollowingListReqVO findFollowingListReqVO);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||
import com.hanserwei.framework.common.exception.ApiException;
|
||||
import com.hanserwei.framework.common.response.PageResponse;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants;
|
||||
@@ -38,6 +41,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -264,6 +268,81 @@ public class RelationServiceImpl implements RelationService {
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<FindFollowingUserRspVO> findFollowingList(FindFollowingListReqVO findFollowingListReqVO) {
|
||||
// 要查询的用户ID
|
||||
Long userId = findFollowingListReqVO.getUserId();
|
||||
// 页码
|
||||
Integer pageNo = findFollowingListReqVO.getPageNo();
|
||||
// 先从Redis中查询
|
||||
String followingRedisKey = RedisKeyConstants.buildUserFollowingKey(userId);
|
||||
// 查询目标用户的关注列表ZSet的总大小
|
||||
Long total = redisTemplate.opsForZSet().zCard(followingRedisKey);
|
||||
log.info("==> 查询目标用户的关注列表ZSet的总大小{}", total);
|
||||
|
||||
|
||||
// 构建回参
|
||||
List<FindFollowingUserRspVO> findFollowingUserRspVOS = null;
|
||||
|
||||
if (total != null) {
|
||||
//缓存有数据
|
||||
//每页展示10条数据
|
||||
long limit = 10L;
|
||||
// 计算一共多少页
|
||||
long totalPage = PageResponse.getTotalPage(total, limit);
|
||||
|
||||
// 请求页码超过总页数
|
||||
if (pageNo > totalPage) {
|
||||
log.info("==> 请求页码超过总页数,返回空数据");
|
||||
return PageResponse.success(null, pageNo, total);
|
||||
}
|
||||
|
||||
// 准备从ZSet中查询分页数据
|
||||
// 每页展示10条数据,计算偏移量
|
||||
long offset = (pageNo - 1) * limit;
|
||||
|
||||
// 使用 ZREVRANGEBYSCORE 命令按 score 降序获取元素,同时使用 LIMIT 子句实现分页
|
||||
// 注意:这里使用了 Double.POSITIVE_INFINITY 和 Double.NEGATIVE_INFINITY 作为分数范围
|
||||
// 因为关注列表最多有 1000 个元素,这样可以确保获取到所有的元素
|
||||
Set<Object> followingUserIdsSet = redisTemplate.opsForZSet()
|
||||
.reverseRangeByScore(followingRedisKey,
|
||||
Double.NEGATIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY,
|
||||
offset,
|
||||
limit);
|
||||
if (CollUtil.isNotEmpty(followingUserIdsSet)) {
|
||||
//提取所有ID
|
||||
List<Long> userIds = followingUserIdsSet.stream()
|
||||
.map(object -> Long.parseLong(object.toString())).toList();
|
||||
|
||||
log.info("==> 批量查询用户信息,用户ID: {}", userIds);
|
||||
|
||||
// RPC: 批量查询用户信息
|
||||
List<FindUserByIdRspDTO> findUserByIdRspDTOS = userRpcService.findByIds(userIds);
|
||||
|
||||
log.info("==> 批量查询用户信息,结果: {}", findUserByIdRspDTOS);
|
||||
|
||||
// 若不为空则,则DTO转换为VO
|
||||
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
|
||||
findFollowingUserRspVOS = findUserByIdRspDTOS.stream().map(findUserByIdRspDTO -> FindFollowingUserRspVO.builder()
|
||||
.userId(findUserByIdRspDTO.getId())
|
||||
.introduction(findUserByIdRspDTO.getIntroduction())
|
||||
.nickname(findUserByIdRspDTO.getNickName())
|
||||
.avatar(findUserByIdRspDTO.getAvatar())
|
||||
.build()).toList();
|
||||
}
|
||||
}else {
|
||||
// TODO: 若 Redis 中没有数据,则从数据库查询
|
||||
|
||||
// TODO: 异步将关注列表全量同步到 Redis
|
||||
}
|
||||
}
|
||||
//noinspection DataFlowIssue
|
||||
return PageResponse.success(findFollowingUserRspVOS,
|
||||
pageNo,
|
||||
total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Lua 脚本结果,根据状态码抛出对应的业务异常
|
||||
* @param result Lua 脚本返回结果
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.hanserwei.hannote.user.relation.biz;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.client.producer.SendCallback;
|
||||
@@ -59,4 +60,69 @@ class MQTests {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试:发送对同一个用户关注、取关 MQ
|
||||
*/
|
||||
@Test
|
||||
void testSendFollowUnfollowMQ() {
|
||||
// 操作者用户ID
|
||||
Long userId = 100L;
|
||||
// 目标用户ID
|
||||
Long targetUserId = 2100L;
|
||||
|
||||
for (long i = 0; i < 100; i++) {
|
||||
if (i % 2 == 0) {
|
||||
// 偶数发送关注 MQ
|
||||
log.info("{} 是偶数", i);
|
||||
|
||||
// 发送 MQ
|
||||
// 构建消息体 DTO
|
||||
FollowUserMqDTO followUserMqDTO = FollowUserMqDTO.builder()
|
||||
.userId(userId)
|
||||
.followUserId(targetUserId)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(followUserMqDTO))
|
||||
.build();
|
||||
|
||||
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||
String destination = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW + ":" + MQConstants.TAG_FOLLOW;
|
||||
|
||||
String hashKey = String.valueOf(userId);
|
||||
|
||||
// 发送 MQ 消息
|
||||
SendResult sendResult = rocketMQTemplate.syncSendOrderly(destination, message, hashKey);
|
||||
|
||||
|
||||
log.info("==> MQ 发送结果,SendResult: {}", sendResult);
|
||||
} else { // 取关发送取关 MQ
|
||||
log.info("{} 是奇数", i);
|
||||
|
||||
// 发送 MQ
|
||||
// 构建消息体 DTO
|
||||
UnfollowUserMqDTO unfollowUserMqDTO = UnfollowUserMqDTO.builder()
|
||||
.userId(userId)
|
||||
.unfollowUserId(targetUserId)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(unfollowUserMqDTO))
|
||||
.build();
|
||||
|
||||
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||
String destination = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW + ":" + MQConstants.TAG_UNFOLLOW;
|
||||
|
||||
String hashKey = String.valueOf(userId);
|
||||
|
||||
// 发送 MQ 消息
|
||||
SendResult sendResult = rocketMQTemplate.syncSendOrderly(destination, message, hashKey);
|
||||
|
||||
log.info("==> MQ 发送结果,SendResult: {}", sendResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user