# 关注 --- 需求: - 开发关注、取消关注功能 - 统计用户的关注数、粉丝数 关键: - 若 A 关注了 B,则 A 是 B 的粉丝 Follower,B 是 A 的目标 Followee - 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体 ## 工具类:生成 Redis 的 Key ```java /** * 生成 Redis 的 key */ public class RedisKeyUtil { private static final String SPLIT = ":"; private static final String PREFIX_FOLLOWER = "follower"; // 被关注(粉丝) private static final String PREFIX_FOLLOWEE = "followee"; // 关注的目标 /** * 某个用户关注的实体 * followee:userId:entityType -> zset(entityId, now) 以当前关注的时间进行排序 * @param userId 粉丝的 id * @param entityType 关注的实体类型 * @return */ public static String getFolloweeKey(int userId, int entityType) { return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; } /** * 某个实体拥有的粉丝 * follower:entityType:entityId -> zset(userId, now) * @param entityType * @param entityId * @return */ public static String getFollowerKey(int entityType, int entityId) { return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId; } } ``` ## Service ```java /** * 关注相关 */ @Service public class FollowService { @Autowired private RedisTemplate redisTemplate; /** * 关注 * @param userId * @param entityType * @param entityId */ public void follow(int userId, int entityType, int entityId) { redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { // 生成 Redis 的 key String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); // 开启事务管理 redisOperations.multi(); // 插入数据 redisOperations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis()); redisOperations.opsForZSet().add(followerKey, userId, System.currentTimeMillis()); // 提交事务 return redisOperations.exec(); } }); } /** * 取消关注 * @param userId * @param entityType * @param entityId */ public void unfollow(int userId, int entityType, int entityId) { redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { // 生成 Redis 的 key String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); // 开启事务管理 redisOperations.multi(); // 删除数据 redisOperations.opsForZSet().remove(followeeKey, entityId); redisOperations.opsForZSet().remove(followerKey, userId); // 提交事务 return redisOperations.exec(); } }); } /** * 查询某个用户关注的实体的数量 * @param userId 用户 id * @param entityType 实体类型 * @return */ public long findFolloweeCount(int userId, int entityType) { String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); return redisTemplate.opsForZSet().zCard(followeeKey); } /** * 查询某个实体的粉丝数量 * @param entityType * @param entityId * @return */ public long findFollowerCount(int entityType, int entityId) { String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); return redisTemplate.opsForZSet().zCard(followerKey); } /** * 判断当前用户是否已关注该实体 * @param userId * @param entityType * @param entityId * @return */ public boolean hasFollowed(int userId, int entityType, int entityId) { String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); return redisTemplate.opsForZSet().score(followeeKey, entityId) != null ; } } ``` ## Controller ```java /** * 关注 */ @Controller public class FollowController { @Autowired private FollowService followService; @Autowired private HostHolder hostHolder; /** * 关注 * @param entityType * @param entityId * @return */ @PostMapping("/follow") @ResponseBody public String follow(int entityType, int entityId) { User user = hostHolder.getUser(); followService.follow(user.getId(), entityType, entityId); return CommunityUtil.getJSONString(0, "已关注"); } /** * 取消关注 * @param entityType * @param entityId * @return */ @PostMapping("/unfollow") @ResponseBody public String unfollow(int entityType, int entityId) { User user = hostHolder.getUser(); followService.unfollow(user.getId(), entityType, entityId); return CommunityUtil.getJSONString(0, "已取消关注"); } } ``` 在 `UserController` 中添加进入个人主页查询关注/粉丝数量的逻辑: ```java // 关注数量 long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER); model.addAttribute("followeeCount", followeeCount); // 粉丝数量 long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId); model.addAttribute("followerCount", followerCount); // 当前登录用户是否已关注该用户 boolean hasFollowed = false; if (hostHolder.getUser() != null) { hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); } model.addAttribute("hasFollowed", hasFollowed); ``` ## 前端 ```html
关注了 关注者
``` 对应的 `profile.js` ```js $(function(){ $(".follow-btn").click(follow); }); function follow() { var btn = this; if($(btn).hasClass("btn-info")) { // 关注TA $.post( CONTEXT_PATH + "/follow", {"entityType":3, "entityId":$(btn).prev().val()}, function (data) { data = $.parseJSON(data); if (data.code == 0) { // 偷个懒,直接刷新界面 window.location.reload(); } else { alert(data.msg); } } ) } else { // 取消关注 $.post( CONTEXT_PATH + "/unfollow", {"entityType":3, "entityId":$(btn).prev().val()}, function (data) { data = $.parseJSON(data); if (data.code == 0) { // 偷个懒,直接刷新界面 window.location.reload(); } else { alert(data.msg); } } ) } } ``` 注意: ``` $.post( CONTEXT_PATH + "/follow", {"entityType":3, "entityId":$(btn).prev().val()}, ``` 中的 `"entityType"` 和 `“entityId”` 名称对应后端 `/follow` 方法的参数,其值需要从前端获取