Echo/docs/200-关注.md
2021-01-28 22:09:02 +08:00

297 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 关注
---
需求:
- 开发关注、取消关注功能
- 统计用户的关注数、粉丝数
关键:
- 若 A 关注了 B则 A 是 B 的粉丝 FollowerB 是 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
<div class="media-body">
<h5 class="mt-0 text-warning">
<span th:utext="${user.username}"></span>
<input type="hidden" id="entityId" th:value="${user.id}">
<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}"></button>
</h5>
<div class="text-muted mt-3 mb-5">
<span>关注了 <a class="text-primary" href="followee.html" th:text="${followeeCount}"></a></span>
<span class="ml-4">关注者 <a class="text-primary" href="follower.html" th:text="${followerCount}"></a></span>
</div>
</div>
```
对应的 `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` 方法的参数,其值需要从前端获取