和大佬互关了,顺便把掘金的关注功能落地了

个人项目:社交支付项目(小老板)

作者:三哥,j3code.cn

项目文档:www.yuque.com/g/j3code/dv...

预览地址(未开发完):admire.j3code.cn/small-boss

  • 内网穿透部署,第一次访问比较慢

1、分析

现在的社交类项目中用户关注功能是必不可少的,该功能能让用户更聚焦于自己中意的那部分内容。就像 B 站关注功能,在你浏览视频时,推送的内容中有相当一部分内容都在自你关注的 UP。当然,我们肯定做不了 B 站那样的效果拉,但是这个关注功能我们还是可以拎出来,实现实现。

就像这样:

这个图片,我们可以得出三个信息:

  • 关注按钮
  • 关注数据项
  • 粉丝数据项

理解起来也不是很难,关注按钮就是我们提供的关注功能,点击就当前登录用户关注该页面用户;关注数据项表示该页面用户关注了那些用户;粉丝数据项就是有多少个用户关注了该页面用户。

大致了解了这三项信息之后,再来看看用户 A 关注用户 B,应该需要记录那些字段。

首先肯定是需要记录这个动作的发起方用户 ID,对吧!咱们把他定义为 user_id。接着我们是不是要记录一下这个动作的接收方,也即被关注者的用户ID,咱们把他定义为 follow_user_id。如果我在加一个 status 字段记录用户关注或者取消关注,是否可行?

可能有人会说,麻烦,多记录这一个干啥!关注就插入一条数据,取消关注就删除这条数据,不就 ok 了。

其实数据的删除和修改本身没太大区别(效率上也没啥区别),而且你不觉得删除的危险性会比修改搞嘛!万一你脑子一热删除数据的时候不带条件,那数据是不是就清空了。而修改的话则数据都还在,只是出现了数据的混乱。对于用户来说有数据总比没有数据强,对吧!

所以这里我就建议关注和取消都修改 status 字段来实现用户的关注功能/取消功能。

基本的字段我们都确定好了,再来看看后面的数据项场景,也即如果我根据这些字段,如何查找一个用户的关注数据?

关注数据查询:

查询一个用户的关注数据,我们只需要根据用户的 id,去 MySQL 中找到 user_id 是传入的 id,且 status 为关注的数据即可,伪 SQL 如下:

select * from 表 where user_id = 用户id and status = 关注

再来看看用户的粉丝数据如何查找

粉丝数据查询:

查询一个用户的粉丝数据,我们只需要根据用户的 id,去 MySQL 中找到 follow_user_id 是传入的 id,且 status 为 关注的数据即可,伪 SQL 如下:

select * from 表 where follow_user_id = 用户id and status = 关注

这样看似非常好,一个关注功能就做好了,如果你们是这么想的话,我只能说 dome 罢了。

2、再分析

按照上面的分析,如果有下面两个场景:

  1. 用户 A 关注用户 B
  2. 用户 B 关注用户 A

是不是会产生两条关注数据,只不过是 user_id 与 follow_user_id 字段的内容对调了一下而已,所以我们能不能考虑将两者合并一下呢!

网上的大部分解决办法是加个互关字段:mutual_follow(0不互关,1互关)。

那,此时你想一下 status 这个字段的值是不是就不单单是关注/非关注这么简单了,对吧!是不是应该有如下状态:

  1. 两者一个都不关注,暂且赋值为 1
  2. user_id 不关注 follow_user_id,但此时 follow_user_id 任然关注 user_id,暂且赋值为 2
  3. follow_user_id 不关注 user_id,但此时 user_id 任然关注 follow_user_id,暂且赋值为 3

现在,按照上面的字段及其含义,来看看如何实现 A、B两个用户的数据查询:

关注数:

这里我直接写伪 SQL:

select * from 表 where user_id = 用户id and (status = 3 or mutual_follow = 1)

union all

select * from 表 where follow_user_id = 用户id and (status = 2 or mutual_follow = 1)

粉丝数:

这里我直接写伪 SQL:

select * from 表 where follow_user_id = 用户id and (status = 3 or mutual_follow = 1)

union all

select * from 表 where user_id= 用户id and (status = 2 or mutual_follow = 1)

看着上面的 SQL 是不是感觉头大,为什么会有这么烧脑的逻辑,反正我要是看着这段 SQL 绝对是要该掉的。因为为了减少数据的插入,而在一行数据中加入这么多的逻辑,我觉得反而不值得。再说了,当系统用户量上来之后,一张表肯定是不可能满足系统要求的,对吧!所以还要考虑分表。

那,如果还按照上面的分析,来进行分表,是不是又陷入困境了,肯本找不到合适的分片字段进行水平分表,所以最终的结果是,这个方案不行。

3、最终分析

我们能不能思路打开,不要只局限一个表,将关注和粉丝两种不同的数据进行分开存储,这不就行了嘛!

用户 A 关注了用户 B,那就再关注表中存入 A 关注 B 的关注数据,在粉丝表中存入 A 是 B 的粉丝数据这不就 ok 了嘛!后期分表也是非常好做的,直接对关注表和粉丝表进行分表,简单而容易理解。

那先来看看关注表字段:

  • user_id:用户id
  • follow_user_id:被关注用户id
  • status:关注状态:1 关注,0 未关注

再来看看粉丝表字段

  • user_id:用户id
  • follower_id:粉丝id
  • status:关注状态:1 关注,0 未关注

此时,如果 A 关注 B 了,表数据的存储如下:

关注表

user_id follow_user_id status
A B 1

粉丝表

user_id follower_id status
B A 1

怎么样,逻辑是不是非常清晰,后期要进行分表的话,也是非常好处理的,直接针对这两张表的 user_id 进行水平分表(因为所有的查都是通过 user_id 来的)。至于查询 SQL 我相信学过 MySQL 的都会写,我就不在这里赘述了。

不过,这里也是有点问题,就是在插入数据和修改数据的时候,我们要同时操作两张表这就要保证整个操作的原子性了,不过这个我觉得还是很好处理的,后面看我代码实现(其实就是加事务)。

最后再来个图结束本次的分析,如下:

4、实现

先来创建一下两张表,SQL 如下:

关注表:

sql 复制代码
CREATE TABLE `sb_user_follow` (
  `id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `follow_user_id` bigint(20) NOT NULL COMMENT '关注的用户id',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态,false未关注,true关注',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `un` (`user_id`,`follow_user_id`),
  KEY `key` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

粉丝表:

sql 复制代码
CREATE TABLE `sb_user_follower` (
  `id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `follower_id` bigint(20) NOT NULL COMMENT '粉丝id',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态,false未关注,true关注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `un` (`user_id`,`follower_id`),
  KEY `key` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

如果系统中本身就就引入了分表组件,我建议直接对这两张表进行分表,省的后期又要过来做分表操作,这里索性一步到位。

:MyBatisX 插件自动生成代码我就不过多赘述,已经讲过很多遍了。

所有前期准备做好只好,来看看关注功能的实现:

1)controller

位置:cn.j3code.community.api.v1.controller

java 复制代码
@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/userFollow")
public class UserFollowController {

    private final UserFollowService userFollowService;

    /**
     * 关注
     *
     * @param userId 用户id
     * @param status 状态
     */
    @GetMapping("/follow")
    public void follow(@RequestParam("userId") Long userId, @RequestParam("status") FollowStatusEnum status) {
        userFollowService.follow(userId, status);
    }
}

FollowStatusEnum 枚举对象

位置:cn.j3code.config.enums

java 复制代码
@Getter
public enum FollowStatusEnum {

    FOLLOW(true, "关注"),
    NOT_FOLLOW(false, "取消关注"),


    ;

    @EnumValue
    private Boolean value;

    private String description;

    FollowStatusEnum(Boolean value, String description) {
        this.value = value;
        this.description = description;
    }
}

2)service

位置:cn.j3code.community.service

java 复制代码
public interface UserFollowService extends IService<UserFollow> {
    void follow(Long userId, FollowStatusEnum status);
}
@Slf4j
@AllArgsConstructor
@Service
public class UserFollowServiceImpl extends ServiceImpl<UserFollowMapper, UserFollow>
    implements UserFollowService {

    private final UserFollowerService userFollowerService;
    private final TransactionTemplate transactionTemplate;

    @Override
    public void follow(Long userId, FollowStatusEnum status) {
        if (userId.equals(SecurityUtil.getUserId())) {
            throw new SysException("真坏,自己还要关注嘛!");
        }

        // 关注对象
        UserFollow follow = new UserFollow();
        follow.setStatus(status.getValue());
        // 我关注了 userId
        follow.setUserId(SecurityUtil.getUserId());
        follow.setFollowUserId(userId);
        follow.setCreateTime(LocalDateTime.now());
        follow.setUpdateTime(LocalDateTime.now());

        // 粉丝对象
        UserFollower follower = new UserFollower();
        follower.setStatus(status.getValue());
        // 我是 userId 的粉丝
        follower.setFollowerId(SecurityUtil.getUserId());
        follower.setUserId(userId);

        MyTransactionTemplate.execute(transactionTemplate, accept -> {
            // 保存关注对象
            getBaseMapper().follow(follow);
            // 保存粉丝对象
            userFollowerService.follower(follower);
        }, status.getDescription() + "失败,稍后重试!");

        if (FollowStatusEnum.NOT_FOLLOW.equals(status)) {
            return;
        }
        // 消息通知,对关注动作
    }
}

3)mapper

UserFollowMapper 实现:

xml 复制代码
<insert id="follow">
    insert into sb_user_follow(user_id,follow_user_id,`status`,create_time,update_time)
    values
        (
            #{follow.userId},
            #{follow.followUserId},
            #{follow.status},
            #{follow.createTime},
            #{follow.updateTime}
        )
        on duplicate key update
        `status` = VALUES(`status`),
        `update_time` = VALUES(`update_time`)
</insert>

UserFollowerMapper 实现:

xml 复制代码
<insert id="follower">
    insert into sb_user_follower(user_id,follower_id,`status`)
    values
        (
            #{follower.userId},
            #{follower.followerId},
            #{follower.status}
        )
        on duplicate key update
        `status` = VALUES(`status`)
</insert>

到此,我们的关注功能算是完结了,至于后期的关注列表数据和粉丝列表数据就非常简单,我相信大家应该可以动手查出来,我就不贴代码了。

相关推荐
骆晨学长3 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
AskHarries8 分钟前
利用反射实现动态代理
java·后端·reflect
@月落9 分钟前
alibaba获得店铺的所有商品 API接口
java·大数据·数据库·人工智能·学习
liuyang-neu15 分钟前
力扣 42.接雨水
java·算法·leetcode
z千鑫18 分钟前
【人工智能】如何利用AI轻松将java,c++等代码转换为Python语言?程序员必读
java·c++·人工智能·gpt·agent·ai编程·ai工具
Flying_Fish_roe32 分钟前
Spring Boot-Session管理问题
java·spring boot·后端
赚钱给孩子买茅台喝34 分钟前
智能BI项目第四期
java·spring boot·spring cloud·aigc
hai405871 小时前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
陈大爷(有低保)2 小时前
UDP Socket聊天室(Java)
java·网络协议·udp
kinlon.liu2 小时前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证