feat(user): 新增用户关注列表查询功能

- 新增查询用户关注列表接口,支持分页查询
- 新增批量查询用户信息接口,提升查询效率
- 优化 MQ 消费模式为顺序消费,确保关注/取关操作有序性
- 完善用户信息 DTO,新增简介字段
- 新增分页响应封装类,支持分页查询结果返回
- 优化 Redis 查询逻辑,支持从缓存中分页获取关注列表
- 新增 Lua 脚本结果类型设置,确保脚本执行结果正确解析
- 添加 HTTP 接口测试用例,覆盖关注列表及批量查询接口
- 实现缓存与数据库双写一致性,提高数据查询性能
This commit is contained in:
2025-10-14 22:29:13 +08:00
parent b70d9073d8
commit 1e350a4af5
16 changed files with 490 additions and 17 deletions

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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 脚本返回结果

View File

@@ -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);
}
}
}
}

View File

@@ -2,16 +2,15 @@ package com.hanserwei.hannote.user.api;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.constant.ApiConstants;
import com.hanserwei.hannote.user.dto.req.FindUserByEmailReqDTO;
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
import com.hanserwei.hannote.user.dto.req.RegisterUserReqDTO;
import com.hanserwei.hannote.user.dto.req.UpdateUserPasswordReqDTO;
import com.hanserwei.hannote.user.dto.req.*;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@FeignClient(name = ApiConstants.SERVICE_NAME)
public interface UserFeignApi {
@@ -52,4 +51,13 @@ public interface UserFeignApi {
*/
@PostMapping(value = PREFIX + "/findById")
Response<FindUserByIdRspDTO> findById(@RequestBody FindUserByIdReqDTO findUserByIdReqDTO);
/**
* 批量查询用户信息
*
* @param findUsersByIdsReqDTO 批量查询信息请求
* @return 响应
*/
@PostMapping(value = PREFIX + "/findByIds")
Response<List<FindUserByIdRspDTO>> findByIds(@RequestBody FindUsersByIdsReqDTO findUsersByIdsReqDTO);
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.hannote.user.dto.req;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindFollowingListReqVO {
@NotNull(message = "查询用户 ID 不能为空")
private Long userId;
@NotNull(message = "页码不能为空")
private Integer pageNo = 1; // 默认值为第一页
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.user.dto.req;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindUsersByIdsReqDTO {
@NotNull(message = "用户 ID 集合不能为空")
@Size(min = 1, max = 10, message = "用户 ID 集合大小必须大于等于 1, 小于等于 10")
private List<Long> ids;
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.user.dto.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindFollowingUserRspVO {
private Long userId;
private String avatar;
private String nickname;
private String introduction;
}

View File

@@ -25,4 +25,9 @@ public class FindUserByIdRspDTO {
* 头像
*/
private String avatar;
/**
* 简介
*/
private String introduction;
}

View File

@@ -4,10 +4,7 @@ import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.biz.service.UserService;
import com.hanserwei.hannote.user.dto.req.FindUserByEmailReqDTO;
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
import com.hanserwei.hannote.user.dto.req.RegisterUserReqDTO;
import com.hanserwei.hannote.user.dto.req.UpdateUserPasswordReqDTO;
import com.hanserwei.hannote.user.dto.req.*;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
@@ -19,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
@Slf4j
@@ -63,4 +62,10 @@ public class UserController {
return userService.findById(findUserByIdReqDTO);
}
@PostMapping("/findByIds")
@ApiOperationLog(description = "批量查询用户信息")
public Response<List<FindUserByIdRspDTO>> findByIds(@Validated @RequestBody FindUsersByIdsReqDTO findUsersByIdsReqDTO) {
return userService.findByIds(findUsersByIdsReqDTO);
}
}

View File

@@ -4,13 +4,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.biz.domain.dataobject.UserDO;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.dto.req.FindUserByEmailReqDTO;
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
import com.hanserwei.hannote.user.dto.req.RegisterUserReqDTO;
import com.hanserwei.hannote.user.dto.req.UpdateUserPasswordReqDTO;
import com.hanserwei.hannote.user.dto.req.*;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import java.util.List;
public interface UserService extends IService<UserDO> {
/**
@@ -52,4 +51,12 @@ public interface UserService extends IService<UserDO> {
* @return 响应结果
*/
Response<FindUserByIdRspDTO> findById(FindUserByIdReqDTO findUserByIdReqDTO);
/**
* 批量根据用户 ID 查询用户信息
*
* @param findUsersByIdsReqDTO 批量查询用户信息请求参数
* @return 响应结果
*/
Response<List<FindUserByIdRspDTO>> findByIds(FindUsersByIdsReqDTO findUsersByIdsReqDTO);
}

View File

@@ -1,11 +1,14 @@
package com.hanserwei.hannote.user.biz.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.enums.DeletedEnum;
import com.hanserwei.framework.common.enums.StatusEnum;
@@ -27,16 +30,17 @@ import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.biz.rpc.DistributedIdGeneratorRpcService;
import com.hanserwei.hannote.user.biz.rpc.OssRpcService;
import com.hanserwei.hannote.user.biz.service.UserService;
import com.hanserwei.hannote.user.dto.req.FindUserByEmailReqDTO;
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
import com.hanserwei.hannote.user.dto.req.RegisterUserReqDTO;
import com.hanserwei.hannote.user.dto.req.UpdateUserPasswordReqDTO;
import com.hanserwei.hannote.user.dto.req.*;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,8 +50,10 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
@@ -296,6 +302,7 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
.id(userDO.getId())
.nickName(userDO.getNickname())
.avatar(userDO.getAvatar())
.introduction(userDO.getIntroduction())
.build();
threadPoolTaskExecutor.submit(() -> {
// 过期时间保底1天+随机秒数,避免缓存雪崩
@@ -304,5 +311,119 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
});
return Response.success(findUserByIdRspDTO);
}
@Override
public Response<List<FindUserByIdRspDTO>> findByIds(FindUsersByIdsReqDTO findUsersByIdsReqDTO) {
List<Long> userIds = findUsersByIdsReqDTO.getIds();
// 构建Redis的Key集合
List<String> userInfoKeys = userIds.stream()
.map(RedisKeyConstants::buildUserInfoKey)
.toList();
// 先从Redis中获取
List<Object> redisValues = redisTemplate.opsForValue().multiGet(userInfoKeys);
// 如果缓存中不为空,过滤掉空值
if (CollUtil.isNotEmpty(redisValues)){
redisValues = redisValues.stream()
.filter(Objects::nonNull)
.toList();
}
// 返参
List<FindUserByIdRspDTO> findUserByIdRspDTOS = Lists.newArrayList();
// 将过滤后的缓存集合,转换为 DTO 返参实体类
if (CollUtil.isNotEmpty(redisValues)) {
findUserByIdRspDTOS = redisValues.stream()
.map(value -> JsonUtils.parseObject(String.valueOf(value), FindUserByIdRspDTO.class))
.collect(Collectors.toList());
}
// 如果被查询的用户信息,都在 Redis 缓存中, 则直接返回
if (CollUtil.size(userIds) == CollUtil.size(findUserByIdRspDTOS)) {
return Response.success(findUserByIdRspDTOS);
}
// 还有另外两种情况:一种是缓存里没有用户信息数据,还有一种是缓存里数据不全,需要从数据库中补充
// 筛选出缓存里没有的用户数据,去查数据库
List<Long> userIdsNeedQuery = null;
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
// 将 findUserInfoByIdRspDTOS 集合转 Map
Map<Long, FindUserByIdRspDTO> map = findUserByIdRspDTOS.stream()
.collect(Collectors.toMap(FindUserByIdRspDTO::getId, p -> p));
// 筛选出需要查 DB 的用户 ID
userIdsNeedQuery = userIds.stream()
.filter(id -> Objects.isNull(map.get(id)))
.toList();
} else { // 缓存中一条用户信息都没查到,则提交的用户 ID 集合都需要查数据库
userIdsNeedQuery = userIds;
}
// 数据库中批量查询用户信息
List<UserDO> userDOs = this.list(new LambdaQueryWrapper<>(UserDO.class)
.eq(UserDO::getStatus, 0)
.eq(UserDO::getIsDeleted, 0)
.in(UserDO::getId, userIdsNeedQuery));
List<FindUserByIdRspDTO> findUserByIdRspDTOS2 = null;
// 若数据不为空,则转为 DTO 返回
if (CollUtil.isNotEmpty(userDOs)){
findUserByIdRspDTOS2 = userDOs.stream()
.map(userDO -> FindUserByIdRspDTO.builder()
.id(userDO.getId())
.nickName(userDO.getNickname())
.introduction(userDO.getIntroduction())
.avatar(userDO.getAvatar())
.build())
.toList();
// 批量更新 Redis
List<FindUserByIdRspDTO> finalFindUserByIdRspDTOS = findUserByIdRspDTOS2;
threadPoolTaskExecutor.submit(()->{
// DTO集合转Map
Map<Long, FindUserByIdRspDTO> map = finalFindUserByIdRspDTOS.stream()
.collect(Collectors.toMap(FindUserByIdRspDTO::getId, p -> p));
// 执行pipeline操作
//noinspection NullableProblems
redisTemplate.executePipelined(new SessionCallback<>() {
@SuppressWarnings("unchecked")
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// 你可以直接获取 ValueOperations
ValueOperations<String, Object> valueOperations = operations.opsForValue();
for (UserDO userDO : userDOs) {
Long userId = userDO.getId();
// 用户缓存Key (K = String)
String userInfoRedisKey = RedisKeyConstants.buildUserInfoKey(userId);
// DTO转JSON
FindUserByIdRspDTO findUserByIdRspDTO = map.get(userId);
// 的值类型是 Object所以它可以接受 String。
String value = JsonUtils.toJsonString(findUserByIdRspDTO);
// 过期时间
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
valueOperations.set(userInfoRedisKey, value, expireSeconds, TimeUnit.SECONDS);
}
return null;
}
});
});
}
// 合并数据
if (CollUtil.isNotEmpty(findUserByIdRspDTOS2)) {
findUserByIdRspDTOS.addAll(findUserByIdRspDTOS2);
}
return Response.success(findUserByIdRspDTOS);
}
}

View File

@@ -0,0 +1,56 @@
package com.hanserwei.framework.common.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 分页响应(未使用任何分页插件)
*
* @author hanserwei
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class PageResponse<T> extends Response<List<T>> {
private long pageNo; // 当前页码
private long totalCount; // 总数据量
private long pageSize; // 每页展示的数据量
private long totalPage; // 总页数
public static <T> PageResponse<T> success(List<T> data, long pageNo, long totalCount) {
PageResponse<T> pageResponse = new PageResponse<>();
pageResponse.setSuccess(true);
pageResponse.setData(data);
pageResponse.setPageNo(pageNo);
pageResponse.setTotalCount(totalCount);
// 每页展示的数据量
long pageSize = 10L;
pageResponse.setPageSize(pageSize);
// 计算总页数
long totalPage = (totalCount + pageSize - 1) / pageSize;
pageResponse.setTotalPage(totalPage);
return pageResponse;
}
public static <T> PageResponse<T> success(List<T> data, long pageNo, long totalCount, long pageSize) {
PageResponse<T> pageResponse = new PageResponse<>();
pageResponse.setSuccess(true);
pageResponse.setData(data);
pageResponse.setPageNo(pageNo);
pageResponse.setTotalCount(totalCount);
pageResponse.setPageSize(pageSize);
// 计算总页数
long totalPage = pageSize == 0 ? 0 : (totalCount + pageSize - 1) / pageSize;
pageResponse.setTotalPage(totalPage);
return pageResponse;
}
/**
* 获取总页数
* @return 总页数
*/
public static long getTotalPage(long totalCount, long pageSize) {
return pageSize == 0 ? 0 : (totalCount + pageSize - 1) / pageSize;
}
}

View File

@@ -155,3 +155,22 @@ Authorization: Bearer {{token}}
{
"unfollowUserId": 2100
}
### 批量查询用户信息
POST http://localhost:8000/user/user/findByIds
Content-Type: application/json
Authorization: Bearer {{token}}
{
"ids": [100,2100,4100]
}
### 查询用户关注列表
POST http://localhost:8000/relation/relation/following/list
Content-Type: application/json
Authorization: Bearer {{token}}
{
"userId": 100,
"pageNo": 1
}