feat(user): 新增用户关注列表查询功能
- 新增查询用户关注列表接口,支持分页查询 - 新增批量查询用户信息接口,提升查询效率 - 优化 MQ 消费模式为顺序消费,确保关注/取关操作有序性 - 完善用户信息 DTO,新增简介字段 - 新增分页响应封装类,支持分页查询结果返回 - 优化 Redis 查询逻辑,支持从缓存中分页获取关注列表 - 新增 Lua 脚本结果类型设置,确保脚本执行结果正确解析 - 添加 HTTP 接口测试用例,覆盖关注列表及批量查询接口 - 实现缓存与数据库双写一致性,提高数据查询性能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user