问答平台(4),关注、取消关注

需求

1
2
- 开发关注、取消关注功能。
- 统计用户的关注数、粉丝数。

关键

1
2
- 若A关注了B,则AB的Follower(粉丝、关注者),BA的Followee(目标、被关注者)。
- 关注的目标可以是用户、帖子等,实现时将这些目标抽象为实体。

工具类

  • RedisKeyUtil:增加内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static final String PREFIX_FOLLOWEE = "followee";
    private static final String PREFIX_FOLLOWER = "follower";

    // 某个用户关注的实体
    // followee:userId:entityType -> zset(entityId, now)
    public static String getFolloweeKey(int userId, int entityType) {
    return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
    }

    // 某个实体拥有的粉丝
    // follower:entityType:entityId -> zset(userId, now)
    public static String getFollowerKey(int entityType, int entityId) {
    return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
    }
  • CommunityConstant:增加内容
    1
    2
    3
    4
    /**
    * 实体类型:用户
    */
    int ENTITY_TYPE_USER = 3;

业务层

  • FollowService:新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    @Service
    public class FollowService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;

    // 关注
    public void follow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
    @Override
    public Object execute(RedisOperations operations) throws DataAccessException {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

    // 启用事务
    operations.multi();

    operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
    operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

    return operations.exec();
    }
    });
    }

    // 取消关注
    public void unfollow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
    @Override
    public Object execute(RedisOperations operations) throws DataAccessException {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

    // 启用事务
    operations.multi();

    operations.opsForZSet().remove(followeeKey, entityId);
    operations.opsForZSet().remove(followerKey, userId);

    return operations.exec();
    }
    });
    }

    // 查询关注实体的数量
    public long findFolloweeCount(int userId, int entityType) {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    return redisTemplate.opsForZSet().zCard(followeeKey);
    }

    // 查询实体的粉丝数量
    public long findFollowerCount(int entityType, int entityId) {
    String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
    return redisTemplate.opsForZSet().zCard(followerKey);
    }

    // 查询当前用户是否已关注该实体
    public boolean hasFollowed(int userId, int entityType, int entityId) {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
    }
    }

表现层

  • FollowController:新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Controller
    public class FollowController implements CommunityConstant {

    @Autowired
    private FollowService followService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/follow", method = RequestMethod.POST)
    @ResponseBody
    public String follow(int entityType, int entityId) {
    User user = hostHolder.getUser();

    followService.follow(user.getId(), entityType, entityId);

    return CommunityUtil.getJSONString(0, "已关注!");
    }

    @RequestMapping(path = "/unfollow", method = RequestMethod.POST)
    @ResponseBody
    public String unfollow(int entityType, int entityId) {
    User user = hostHolder.getUser();

    followService.unfollow(user.getId(), entityType, entityId);

    return CommunityUtil.getJSONString(0, "已取消关注!");
    }
    }
  • UserController:修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Autowired
    private FollowService followService;

    // 个人主页
    @RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)
    public String getProfilePage(@PathVariable("userId") int userId, Model model) {
    User user = userService.findUserById(userId);
    if (user == null) {
    throw new RuntimeException("该用户不存在!");
    }

    // 用户
    model.addAttribute("user", user);
    // 点赞数量
    int likeCount = likeService.findUserLikeCount(userId);
    model.addAttribute("likeCount", likeCount);

    // 关注数量
    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);

    return "/site/profile";
    }

页面

  • profile.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- 内容 -->
    <!-- 个人信息 -->
    <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:if="${loginUser!=null&&loginUser.id!=user.id}"
    th:text="${hasFollowed?'已关注':'关注TA'}">关注TA
    </button>
    <span>关注了 <a class="text-primary" th:href="@{|/followees/${user.id}|}"th:text="${followeeCount}">5</a></span>
    <span class="ml-4">关注者 <a class="text-primary" th:href="@{|/followers/${user.id}|}" th:text="${followerCount">123</a></span>
    <span class="ml-4">获得了 <i class="text-danger" th:text="${likeCount}">87</i> 个赞</span>
  • profile.js:修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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);
    }
    }
    );
    // $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary");
    } 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);
    }
    }
    );
    // $(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
    }
    }

问答平台(4),关注、取消关注
https://lcf163.github.io/2020/05/26/问答平台(4),关注、取消关注/
作者
乘风的小站
发布于
2020年5月26日
许可协议