diff --git a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/consumer/FollowUnfollowConsumer.java b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/consumer/FollowUnfollowConsumer.java index c19a0f7..6844ab8 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/consumer/FollowUnfollowConsumer.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/consumer/FollowUnfollowConsumer.java @@ -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 { // Lua脚本 DefaultRedisScript 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); diff --git a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/controller/RelationController.java b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/controller/RelationController.java index 43ba2b0..1fba120 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/controller/RelationController.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/controller/RelationController.java @@ -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 findFollowingList(@Validated @RequestBody FindFollowingListReqVO findFollowingListReqVO) { + return relationService.findFollowingList(findFollowingListReqVO); + } } \ No newline at end of file diff --git a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/rpc/UserRpcService.java b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/rpc/UserRpcService.java index d56f4cd..70b358f 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/rpc/UserRpcService.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/rpc/UserRpcService.java @@ -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 findByIds(List userIds) { + FindUsersByIdsReqDTO findUsersByIdsReqDTO = new FindUsersByIdsReqDTO(); + findUsersByIdsReqDTO.setIds(userIds); + + Response> response = userFeignApi.findByIds(findUsersByIdsReqDTO); + + if (!response.isSuccess() || Objects.isNull(response.getData()) || CollUtil.isEmpty(response.getData())) { + return null; + } + + return response.getData(); + } } \ No newline at end of file diff --git a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/RelationService.java b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/RelationService.java index fa9de15..11fa37c 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/RelationService.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/RelationService.java @@ -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 findFollowingList(FindFollowingListReqVO findFollowingListReqVO); } diff --git a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/impl/RelationServiceImpl.java b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/impl/RelationServiceImpl.java index 72e081e..515d07a 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/impl/RelationServiceImpl.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/main/java/com/hanserwei/hannote/user/relation/biz/service/impl/RelationServiceImpl.java @@ -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 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 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 followingUserIdsSet = redisTemplate.opsForZSet() + .reverseRangeByScore(followingRedisKey, + Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY, + offset, + limit); + if (CollUtil.isNotEmpty(followingUserIdsSet)) { + //提取所有ID + List userIds = followingUserIdsSet.stream() + .map(object -> Long.parseLong(object.toString())).toList(); + + log.info("==> 批量查询用户信息,用户ID: {}", userIds); + + // RPC: 批量查询用户信息 + List 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 脚本返回结果 diff --git a/han-note-user-relation/han-note-user-relation-biz/src/test/java/com/hanserwei/hannote/user/relation/biz/MQTests.java b/han-note-user-relation/han-note-user-relation-biz/src/test/java/com/hanserwei/hannote/user/relation/biz/MQTests.java index e9f3d0b..364414d 100644 --- a/han-note-user-relation/han-note-user-relation-biz/src/test/java/com/hanserwei/hannote/user/relation/biz/MQTests.java +++ b/han-note-user-relation/han-note-user-relation-biz/src/test/java/com/hanserwei/hannote/user/relation/biz/MQTests.java @@ -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 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 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); + } + } + } + } \ No newline at end of file diff --git a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/api/UserFeignApi.java b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/api/UserFeignApi.java index 04332f6..c625038 100644 --- a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/api/UserFeignApi.java +++ b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/api/UserFeignApi.java @@ -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 findById(@RequestBody FindUserByIdReqDTO findUserByIdReqDTO); + + /** + * 批量查询用户信息 + * + * @param findUsersByIdsReqDTO 批量查询信息请求 + * @return 响应 + */ + @PostMapping(value = PREFIX + "/findByIds") + Response> findByIds(@RequestBody FindUsersByIdsReqDTO findUsersByIdsReqDTO); } \ No newline at end of file diff --git a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindFollowingListReqVO.java b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindFollowingListReqVO.java new file mode 100644 index 0000000..ece6f8b --- /dev/null +++ b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindFollowingListReqVO.java @@ -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; // 默认值为第一页 +} \ No newline at end of file diff --git a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindUsersByIdsReqDTO.java b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindUsersByIdsReqDTO.java new file mode 100644 index 0000000..7c40bb5 --- /dev/null +++ b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/req/FindUsersByIdsReqDTO.java @@ -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 ids; + +} \ No newline at end of file diff --git a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindFollowingUserRspVO.java b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindFollowingUserRspVO.java new file mode 100644 index 0000000..fdac38a --- /dev/null +++ b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindFollowingUserRspVO.java @@ -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; + +} \ No newline at end of file diff --git a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindUserByIdRspDTO.java b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindUserByIdRspDTO.java index 0fb9df0..7013214 100644 --- a/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindUserByIdRspDTO.java +++ b/han-note-user/han-note-user-api/src/main/java/com/hanserwei/hannote/user/dto/resp/FindUserByIdRspDTO.java @@ -25,4 +25,9 @@ public class FindUserByIdRspDTO { * 头像 */ private String avatar; + + /** + * 简介 + */ + private String introduction; } \ No newline at end of file diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/controller/UserController.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/controller/UserController.java index 98553b4..9198986 100644 --- a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/controller/UserController.java +++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/controller/UserController.java @@ -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> findByIds(@Validated @RequestBody FindUsersByIdsReqDTO findUsersByIdsReqDTO) { + return userService.findByIds(findUsersByIdsReqDTO); + } + } \ No newline at end of file diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/UserService.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/UserService.java index 986e72c..bec16c8 100644 --- a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/UserService.java +++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/UserService.java @@ -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 { /** @@ -52,4 +51,12 @@ public interface UserService extends IService { * @return 响应结果 */ Response findById(FindUserByIdReqDTO findUserByIdReqDTO); + + /** + * 批量根据用户 ID 查询用户信息 + * + * @param findUsersByIdsReqDTO 批量查询用户信息请求参数 + * @return 响应结果 + */ + Response> findByIds(FindUsersByIdsReqDTO findUsersByIdsReqDTO); } \ No newline at end of file diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java index 6b37dd2..0244633 100644 --- a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java +++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java @@ -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 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 implement }); return Response.success(findUserByIdRspDTO); } + + @Override + public Response> findByIds(FindUsersByIdsReqDTO findUsersByIdsReqDTO) { + List userIds = findUsersByIdsReqDTO.getIds(); + + // 构建Redis的Key集合 + List userInfoKeys = userIds.stream() + .map(RedisKeyConstants::buildUserInfoKey) + .toList(); + + // 先从Redis中获取 + List redisValues = redisTemplate.opsForValue().multiGet(userInfoKeys); + + // 如果缓存中不为空,过滤掉空值 + if (CollUtil.isNotEmpty(redisValues)){ + redisValues = redisValues.stream() + .filter(Objects::nonNull) + .toList(); + } + + // 返参 + List 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 userIdsNeedQuery = null; + if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) { + // 将 findUserInfoByIdRspDTOS 集合转 Map + Map 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 userDOs = this.list(new LambdaQueryWrapper<>(UserDO.class) + .eq(UserDO::getStatus, 0) + .eq(UserDO::getIsDeleted, 0) + .in(UserDO::getId, userIdsNeedQuery)); + List 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 finalFindUserByIdRspDTOS = findUserByIdRspDTOS2; + threadPoolTaskExecutor.submit(()->{ + // DTO集合转Map + Map 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 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); + } } diff --git a/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/response/PageResponse.java b/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/response/PageResponse.java new file mode 100644 index 0000000..b22a4a6 --- /dev/null +++ b/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/response/PageResponse.java @@ -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 extends Response> { + private long pageNo; // 当前页码 + private long totalCount; // 总数据量 + private long pageSize; // 每页展示的数据量 + private long totalPage; // 总页数 + + public static PageResponse success(List data, long pageNo, long totalCount) { + PageResponse 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 PageResponse success(List data, long pageNo, long totalCount, long pageSize) { + PageResponse 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; + } +} diff --git a/http-client/gateApi.http b/http-client/gateApi.http index 45ccf0b..17244e4 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -154,4 +154,23 @@ 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 } \ No newline at end of file