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

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