参考牛客网高级项目教程
狂神说Redis教程笔记
功能需求
- 1.在用户的个人信息页面,点击关注,可以关注该用户,并将关注数据用redis存储
- 在关注状态下,再次点击,会取消关注
- 统计该用户关注了多少人,以及粉丝有多少人
- 与点赞功能类似,数据特点是数据量大、变化快、且数据字段较少,因此采用redis储存比较合适
1. dao层设计redis对应的key
- 若A关注了B,则A是B的fans(粉丝),B是A的Followee(目标)。
- 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体。
- 若关注的目标是帖子、题目等,也就是收藏,直接根据实体类型和id确定,今后开发收藏功能也可以复用
设计储存关注对象信息的健值对
key
-
与点赞不同,点赞不需要查询统计我一共点了多少赞,故,不需要储存目标entityId,但关注需要
-
故,关注需要指明实体类型,对人是关注,对其他实体则是是收藏
-
关注需要指明当前用户的userId,这样方便被关注者统计多少粉丝,
- 因为value不能放userId,要放被关注目标的id,这样,方便统计关注了多少人
// 实体类型为user时,为某人关注了某人 // 实体类型为帖子时,为某人收藏了某帖子 follow:target:userId:entityType
value
-
value用有序集合Zset储存关注目标的id,和分数,分数以关注时间表示
- 这样,便于统计关注了多少实体,以及这些实体展示时,可以按照一定规则排序
/** * 某个用户关注的实体,要保存关注的实体类型,还要保存是谁关注的,方便被关注者统计, * 但也要指明被关注的对象具体id,放入value * follow:target:userId:entityType -> zset(entityId,now) * @param userId 当前用户的id * @param entityType 当前用户关注对象的实体类型 * @return 储存关注对象信息的key */ public static String getFollowTarget(int userId, int entityType) { return PREFIX_FOLLOW_TARGET + SPLIT + userId + SPLIT + entityType; }
设计储存粉丝信息的健值对
key
- 与点赞逻辑类似,要储存某个实体的粉丝信息,即某个实体收到的关s注或收到的收藏的粉丝信息
- 因此,要指明这个实体的类型和id
- value中储存粉丝的id,因此能发出关注或收藏的主体只能是user类型,因此只需储存userId即可
value
- 要储存粉丝的id,为今后方便将粉丝按照一定规则罗列出来,用zset还要储存一个对应的分数,用关注时间表示
/**
* 某个实体的粉丝
* follow:fans:entityType:entityId -> zset(userId,now)
* @param entityType 要储存的实体类型
* @param entityId 要储存的实体id
* @return 返回储存实体类型粉丝信息的key
*/
public static String getFollowFans(int entityType, int entityId) {
return PREFIX_FOLLOW_FANS + SPLIT + entityType + SPLIT + entityId;
}
2. Service层处理关注和取关的业务
1. 触发关注、取关事件-redis事务处理
-
可以与之前的点赞逻辑一样,先判断当前用户是否关注了目的对象,根据关注状态处理不同的事件
-
也可以结合前端设计模块触发不同的事件,
- 即关注与取关前端按钮不同,点击不同按钮会触发不同事件,
- 无需在一个按钮触发后,进行查询判断,本例中采用此种策略
-
注意,点击关注或取关后,目标对象的粉丝信息也会发生改变,因此,这两个事件应该放在一个事务中处理
opsForZSet().add(key, value)
opsForZSet().remove(key, value)
/**
* 关注实体事件-被关注对象的粉丝信息也同时更新-增加redis事件处理
* @param userId 当前用户id
* @param entityType 关注对象类型
* @param entityId 关注对象id
*/
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);
// 开启事务
operations.multi();
// 储存关注对象信息-关注对象的粉丝信息
operations.opsForZSet().add(followTargetKey, entityId, System.currentTimeMillis());
operations.opsForZSet().add(followFans, userId, System.currentTimeMillis());
// 提交事务并返回
return operations.exec();
}
});
}
/**
* 取消关注实体事件-被关注对象的粉丝信息也同时更新-增加redis事件处理
* @param userId 当前用户id
* @param entityType 关注对象类型
* @param entityId 关注对象id
*/
public void unFollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);
// 开启事务
operations.multi();
// 储存关注对象信息-关注对象的粉丝信息
operations.opsForZSet().remove(followTargetKey, entityId);
operations.opsForZSet().remove(followFans, userId);
// 提交事务并返回
return operations.exec();
}
});
}
2. 查询关注对象的数量
opsForZSet().size(key)
- 与opsForZSet().zCard(key)功能一样,查询成员数量
/**
* 获取当前用户指定类型关注对象的数量
* @param userId
* @param entityType
* @param entityId
* @return
*/
public long findFollowTargetCnt(int userId, int entityType, int entityId) {
String followTarget = RedisKeyUtil.getFollowTarget(userId, entityType);
return redisTemplate.opsForZSet().size(followTarget);
}
3. 查询当前对象的粉丝数量
/**
* 获取当前用户的粉丝数
* @param entityType
* @param entityId
* @return
*/
public long findFollowFans(int entityType, int entityId) {
String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followFans);
}
4. 查询当前用户是否对目标用户的关注状态
opsForZSet().score(key, member)
- 通过查询成员函数的分数是否存在,来判断menber是否在集合中
/**
* 判断当前用户userid是否关注了目标对象entityId-通过判断目标对象的粉丝中有无当前对象
* @param userId
* @param entityType
* @param entityId
* @return
*/
public boolean hasFollowed(int userId, int entityType, int entityId) {
String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
return redisTemplate.opsForZSet().score(followTargetKey, entityId) != null;
}
3.Controller层处理请求
1. 处理关注、取关异步请求
- 要拦截判断是否登录
- 从拦截器中的当前线程容器中获取登录用户的id
/**
* 处理关注的异步请求
* @param entityType 关注对象类型
* @param entityId 关注对象id
* @return
*/
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
if(user == null) {
throw new IllegalArgumentException("用户没有登录!");
}
followService.follow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "关注成功!");
}
/**
* 处理取消关注的异步请求
* @param entityType
* @param entityId
* @return
*/
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
if(user == null) {
throw new IllegalArgumentException("用户没有登录!");
}
followService.unFollow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已取消关注!");
}
2. 更新访问用户个人主页的请求
-
注意:获取当前用户信息时,要先判断是否为null,未登录状态也可以访问指定用户主页
// 获取当前用户对指定用户的关注状态 boolean hasFollowed = false; if(hostHolder.getUser() != null) { hasFollowed = followService.hasFollowed( hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); } model.addAttribute("hasFollowed", hasFollowed);
-
整体代码如下:
/**
* 访问指定用户个人主页的请求
* @param userId 指定的用户id
* @param model
* @return
*/
@RequestMapping(value = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
// 先获取要访问的用户
User user = userService.findUserById(userId);
if(user == null) {
throw new IllegalArgumentException("该用户不存在!");
}
// 将指定用户信息封装
model.addAttribute("user", user);
// 获取指定用户的点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);
// 获取指定用户的关注对象数量
long followTargetCnt = followService.findFollowTargetCnt(userId, ENTITY_TYPE_USER);
model.addAttribute("followTargetCnt", followTargetCnt);
// 获取指定用户的粉丝数量
long followFans = followService.findFollowFans(ENTITY_TYPE_USER, userId);
model.addAttribute("followFans", followFans);
// 获取当前用户对指定用户的关注状态
boolean hasFollowed = false;
if(hostHolder.getUser() != null) {
hasFollowed = followService.hasFollowed(
hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);
return "/site/profile";
}
4. View层处理模板页面
1.处理关注、取关事件按钮
按钮样式根据关注状态动态改变
th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
关注的显示根据关注状态动态显示
- 如果用户没有登录或者访问的就是自己的主页,无需显示关注的按钮
<button type="button"
th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
th:text="${hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null && loginUser.id!=user.id}">关注TA
</button>
关注事件对应的js文件定义
在按钮标签前添加隐藏标签
- 为了传入指定用户id,在按钮标签前添加隐藏标签
<input type="hidden" id="entityId" th:value="${user.id}">
$(btn).prev().val()
- 取到指定按钮标签前一个标签的内容
$(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);
}
}
);
}
}
2. 显示指定用户关注了多少人
<span>关注了 <a class="text-primary" href="followee.html" th:text="${followTargetCnt}">5</a> 人</span>
3. 显示指定用户的粉丝数
<span class="ml-4">粉丝数 <a class="text-primary" href="follower.html" th:text="${followFans}">123</a> 人</span>
测试结果:
-
未登录状态下,查看某用户个人主页
-
登录状态下,
- 访问自己的主页
- 访问别人的主页
-
点击关注按钮
-
点击取消关注按钮